go-mmproxy: Anwendung um PROXY-Protokoll erweitern

Für ein Projekt benötigten wir einen mandantenfähig Wowza Streaming Engine Server, wobei der Anwendung für einige Features die öffentliche IP-Adresse des Clients bekannt sein muss (z.B. zur Beschränkung des Startens von Live-Streams). Bei unserem Setup erhält die Applikation den Traffic jedoch von der internen IP-Adresse eines Gateways.

Das Problem

Wir betreiben den Wowza Server aus Sicherheitsgründen und für ein Failover-Setup hinter einer Firewall bzw. einem Gateway. Ein Zugriffsschutz wird dabei durch iptables umgesetzt und sämtlicher Traffic durch einen HAProxy geleitet, der auch ein automatisches Failover zwischen mehreren Wowza Servern im Backend ermöglicht.

Bei vorgeschaltetem HAProxy erhält eine Applikation im Backend den Traffic jedoch von der internen IP-Adresse des Gateways – die Applikationen im internen Netz kennen die originale IP-Adresse des Clients somit nicht. Einer Web-Anwendung im internen Netz wird normalerweise bei Bedarf über einen HTTP Request Header (X-Forwarded-For) die Public IP eines Clients übergeben. Diese Lösung funktioniert allerdings nicht für Wowza.

Lösungswege

  • NAT: Mit iptables könnten die IP-Pakete an den Wowza auf der Firewall geändert werden, sodass dieser die öffentliche IP-Adresse eines Clients erkennt – allerdings müssten wir dann ein automatisches Failover zwischen mehreren Backend-Servern z.B. mit IPVS implementieren. Wir möchten aus Sicherheitsgründen aber weiter HAProxy zur Filterung des eingehenden Traffics verwenden.
  • Password Protection: Eine Mandantenfähigkeit und z.B. eine Limitierung des Startens von Live-Streams im Wowza Server nur für bestimmte Clients, könnte mit einer RTMP-Authentication umgesetzt werden – jedoch müssten alle in Wowza integrierten Applikationen oder Streaming-Appliance angepasst werden und diese RTMP-Authentication auch implementiert haben. Außerdem wäre dies eine spezifische Lösung nur für Wowza.
  • go-mmproxy: Das Tool modifiziert lokal den empfangenen Traffic basierend auf dem HAProxy PROXY Protocol, sodass die an eine Applikation weitergeleiteten IP-Pakete wieder die originale Client-IP und auch den verwendeten Port enthalten.

Wir haben uns zur Lösung für go-mmproxy entschieden, da es die geringsten Auswirkungen auf unsere Infrastruktur sowie die Anwendungen hat, sich gut in das bestehende Setup integriert und alle Features von Wowza weiter genutzt werden können.

go-mmproxy und PROXY Protocol

Protokoll und Tooling

Das HAProxy PROXY Protocol sendet nach dem Aufbau der TCP-Verbindung als erstes einen Header mit Meta-Informationen zur ursprünglichen TCP-Verbindung. Dieser enthält die Source- und Destination-IP-Adresse und -Ports, siehe Protokollspezifikation. So können an den HAProxy angebundene Applikationen, die das PROXY Protocol unterstützen, diese Meta-Informationen auslesen und anschließend die Nutzdaten verarbeiten.

Wireshark mit Traffic von HAProxy zum PROXY Protocol

Viele Applikationen, wie auch Wowza, unterstützen das PROXY Protocol von HAProxy allerdings nicht. go-mmproxy ist eine eigenständige Applikation, die Traffic über das PROXY-Protocol von HAProxy annimmt, den Header liest, diesen entfernt und dann Pakete mit der ursprünglichen IP-Adresse erzeugt.

Vergleich der Protokolle: Weitergabe Public Source IP und Port eines Clients an Applikationen im Backend

Die Kommunikation zwischen go-mmproxy und Applikation erfolgt hierbei über das Loopback Interface, sodass go-mmproxy und die abhängige Applikation auf dem selben System laufen müssen. Nach dem Startup von go-mmproxy müssen spezielle Routing-Regeln erstellt werden, damit der Outgoing Traffic der Applikation wieder bei go-mmproxy ankommt:

ip rule add from 127.0.0.1/8 iif lo table 123
ip route add local 0.0.0.0/0 dev lo table 123

Technisch funktioniert die Kommunikation über das Loopback Interface wie folgt:

  1. go-mmproxy erhält Traffic im PROXY Protocol Format vom vorgeschalteten HAProxy und extrahiert die verwendete IP und Port des Client.
  2. go-mmproxy erzeugt einen Socket mit der Option IP_TRANSPARENT und generiert IP-Pakete mit ursprünglicher Source IP (z.B. 203.0.113.25) und Port des Clients. Die Pakete werden wegen des Ziels 127.0.0.1 über das Loopback Interface an die eigentliche Applikation gesendet.
  3. Die Applikation sendet einen Response mit der öffentlichen Destination IP 203.0.113.25. Da die TCP-Verbindung durch go-mmproxy mit 127.0.0.1 ausgebaut wurde, kommt der ausgehende Traffic auf dem Loopback Interface herein. Durch die hinterlegten Routing-Regeln wird sichergestellt, dass der Traffic nicht über die Default Route rausgesendet wird, sondern lokal wieder an den go-mmproxy zugestellt wird.
  4. go-mmproxy sendet den Traffic über den vorgeschalteten HAProxy zurück an den Client.

Installation und Konfiguration

go-mmproxy liegt als Source Code bei GitHub vor – zum Kompilieren ist Go in der Version >= 1.11 erforderlich. Hier ein Beispiel zur Installation auf einem System mit Debian 10:

apt-get install -y golang
go get github.com/path-network/go-mmproxy

Bei der Konfiguration von HAProxy ist der Parameter send-proxy bei den Backend-Servern wichtig, damit zur Kommunikation das PROXY-Protocol verwendet wird:

listen go_mmproxy
  bind 203.0.113.1:1935
  mode tcp
  default-server check inter 10000 fall 2 rise 2 send-proxy
  server server1 10.20.30.15:1937 
  server server2 10.20.30.16:1937 backup

Die Konfiguration und das Service-Management für go-mmproxy kann zusammen in einem systemd Service File erfolgen:

[Unit]
Description=go-mmproxy
[...]

[Service]
Type=simple
LimitNOFILE=65535
ExecStart=/usr/local/sbin/go-mmproxy -l 0.0.0.0:1937 -4 127.0.0.1:1935 -allowed-subnets /etc/go-mmproxy/allowed-subnets
ExecStartPost=/sbin/ip rule add from 127.0.0.1/8 iif lo table 123
ExecStartPost=/sbin/ip route add local 0.0.0.0/0 dev lo table 123
ExecStopPost=/sbin/ip rule del from 127.0.0.1/8 iif lo table 123
ExecStopPost=/sbin/ip route del local 0.0.0.0/0 dev lo table 123
[...]

Dem Parameter -allowed-subnets wird als Argument ein File (hier /etc/go-mmproxy/allowed-subnets) übergeben, in dem IP-Adressen im CIDR-Format aufgelistet werden, von denen aus Traffic vom Listen-Port (hier 0.0.0.0:1937) angenommen und an den Ziel-Port (hier 127.0.0.1:1935) weitergeleitet werden darf.

Die Applikation im Backend sieht nun die ursprüngliche öffentliche IP eines Clients. In Wowza können man nun z.B. für empfangene Streams Client Restrictions definieren, sodass nur bestimmte IP-Adressen einen Stream starten können (Only allow publishing from the following IP addresses).

Performance

Laut den Benchmarks des Upstreams ist bei Einsatz von go-mmproxy mit ca. 14% weniger Traffic-Durchsatz zu rechnen.

Bei unseren Performance-Tests benötigte go-mmproxy je 1Mbit/s durchgeleitetem TCP-Traffic ca. 1% einer CPU-Core (Intel E3-1270 v5); der benötigte RAM war hierbei extrem gering.

Fazit

Für uns funktioniert die Lösung mit go-mmproxy sehr gut. Wir haben nach längerem produktiven Betrieb keine Performance-Engpässe oder Störungen im Zusammenhang mit Wowza und RTMP-Traffic festgestellt.

Sollten in Zukunft weitere TCP/UDP-basierte Applikationen die originale Source IP oder Port eines Clients benötigen, dann werden wir go-mmproxy wieder einsetzen.

Und falls dieser Artikel Dein Interesse geweckt hat und Du dich fragst was wir sonst noch so machen: we are hiring 😉