<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Loki on Kabelbruch</title><link>https://kabelbruch.de/tags/loki/</link><description>Recent content in Loki on Kabelbruch</description><image><title>Kabelbruch</title><url>https://kabelbruch.de/og-image.png</url><link>https://kabelbruch.de/og-image.png</link></image><generator>Hugo</generator><language>de-DE</language><lastBuildDate>Fri, 06 Nov 2026 17:20:00 +0100</lastBuildDate><atom:link href="https://kabelbruch.de/tags/loki/index.xml" rel="self" type="application/rss+xml"/><item><title>Ein bisschen Internet abgeben: Tor-Snowflake-Proxy in der DMZ</title><link>https://kabelbruch.de/posts/tor-snowflake-proxy-dmz/</link><pubDate>Fri, 06 Nov 2026 17:20:00 +0100</pubDate><guid>https://kabelbruch.de/posts/tor-snowflake-proxy-dmz/</guid><description>Tor-Snowflake-Proxy als kleine Docker-VM in der DMZ: eigene Firewall-Regeln, Logs in Grafana – und warum ich die Verbindungsdaten bewusst nicht speichere.</description><content:encoded><![CDATA[<p>Mein Internetanschluss langweilt sich die meiste Zeit. Mit einem Snowflake-Proxy hilft er jetzt Menschen, die hinter Zensur sitzen, ins Tor-Netz zu kommen – sauber abgeschottet in einer eigenen DMZ und mit einem Dashboard, das zählt, ohne mitzuschreiben.</p>
<h2 id="worum-gehts">Worum geht&rsquo;s?</h2>
<p>Inspiriert hat mich Jan mit <a href="https://klomp.de/escucos-techblog/99-tor-snowflake-proxy-mit-docker-auf-synology-nas-installieren/" target="_blank" rel="noopener noreferrer">seinem Artikel</a>: Er betreibt einen Snowflake-Proxy auf seiner Synology. Das wollte ich auch machen – allerdings nicht auf einem NAS, sondern als eigene VM mit Docker, mit eigener Firewall-Zone und mit Logs in meinem Grafana.</p>
<p>Kurz zur Einordnung: <strong>Tor</strong> ist ein Anonymisierungsnetz, das Verbindungen über mehrere Stationen leitet. In vielen Ländern sind die bekannten Tor-Server gesperrt. Hier kommt <strong>Snowflake</strong> ins Spiel: Freiwillige betreiben kleine Proxys, die zensierten Nutzern als Sprungbrett zu einer Tor-Brücke dienen. Die Verbindung läuft per <strong>WebRTC</strong> (dieselbe Technik wie bei Videocalls im Browser) und sieht damit für Zensoren ziemlich unauffällig aus. Weil die Proxys ständig kommen und gehen, lassen sie sich kaum alle sperren – daher der Name: Schneeflocken.</p>
<p>Wichtig, weil das oft verwechselt wird: Ein Snowflake-Proxy ist <strong>kein Exit-Knoten</strong>. Er leitet nur verschlüsselten Tor-Verkehr zwischen Nutzer und Brücke weiter. Er sieht weder, welche Seiten jemand aufruft, noch tritt meine IP-Adresse als Absender irgendwelcher Anfragen ins Internet auf. Und er braucht <strong>keine Portweiterleitung</strong> – alles läuft ausgehend.</p>
<h2 id="container-auf-dem-docker-host--oder-doch-nicht">Container auf dem Docker-Host – oder doch nicht?</h2>
<p>Mein erster Plan war simpel: ein weiterer Container auf meinem Docker-Host. Der läuft eh, da passt noch einer drauf.</p>
<p>Beim zweiten Nachdenken hat mir das nicht mehr gefallen. Auf demselben Host laufen Dienste wie mein Passwort-Manager. Der Snowflake-Proxy redet den ganzen Tag mit fremden Rechnern im Internet. Selbst wenn das Risiko klein ist: Ich will nicht, dass sich so ein Dienst Kernel und Netz mit meinen sensibelsten Daten teilt. Und im Server-Netz kann ich ihm auf der Firewall keine eigenen Regeln geben.</p>
<p>Also habe ich mich für eine <strong>kleine eigene VM in der DMZ</strong> entschieden. Die DMZ hatte ich schon für meinen <a href="/posts/honeypot-t-pot-zuhause/">Honeypot</a> gebaut – und nach dessen Ende wieder abgerissen. Jetzt bekommt sie ein zweites Leben, diesmal mit einem deutlich friedlicheren Bewohner.</p>
<h2 id="voraussetzungen">Voraussetzungen</h2>
<ul>
<li><strong>Hypervisor:</strong> Proxmox VE 9</li>
<li><strong>VM:</strong> 2 vCPU, 1 GB RAM (ohne Ballooning), 16 GB Platte</li>
<li><strong>Betriebssystem:</strong> Debian 13 netinst, nur SSH-Server und Standard-Werkzeuge</li>
<li><strong>Docker:</strong> Docker Engine 29 mit Compose v5 aus dem offiziellen Docker-Repository</li>
<li><strong>Snowflake:</strong> offizielles Image <code>thetorproject/snowflake-proxy</code></li>
<li><strong>Firewall:</strong> FortiGate mit FortiOS 7.6, eigenes VLAN als DMZ</li>
<li><strong>Logging:</strong> Grafana, Loki und Grafana Alloy (als Syslog-Empfänger) in einem eigenen LXC-Container</li>
</ul>
<h2 id="umsetzung">Umsetzung</h2>
<h3 id="schritt-1-die-dmz-wiederbeleben">Schritt 1: Die DMZ wiederbeleben</h3>
<p>Die DMZ ist ein eigenes VLAN mit eigenem Netz. Im Beispiel nenne ich es <code>10.0.99.0/24</code>, das Gateway ist die FortiGate auf <code>.254</code>, die VM bekommt <code>10.0.99.10</code>.</p>
<p>Ein Detail, über das ich gestolpert bin: Beim Rückbau des Honeypots hatte ich das VLAN auch im UniFi-Controller gelöscht. Ohne das Netz dort lassen die Switches das VLAN nicht durch – die VM hätte also ins Leere gefunkt. Also zuerst das VLAN im Controller wieder anlegen (reines VLAN, ohne DHCP und ohne Gateway, das macht ja die FortiGate).</p>
<p>Die Regeln auf der FortiGate, sinngemäß:</p>
<table>
	<thead>
			<tr>
					<th>Richtung</th>
					<th>Was</th>
					<th>Erlaubt?</th>
					<th>Log</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>DMZ → Internet</td>
					<td>alles außer private Netze</td>
					<td>ja, mit AV, Web- und DNS-Filter, IPS</td>
					<td>nur Sicherheitsereignisse</td>
			</tr>
			<tr>
					<td>DMZ → Log-Server</td>
					<td>nur Syslog (TCP 1514)</td>
					<td>ja</td>
					<td>ja</td>
			</tr>
			<tr>
					<td>DMZ → private Netze</td>
					<td>alles andere</td>
					<td><strong>nein</strong></td>
					<td>ja, jeder Treffer ist verdächtig</td>
			</tr>
			<tr>
					<td>Client-Netz / VPN → DMZ</td>
					<td>SSH und Ping</td>
					<td>ja</td>
					<td>ja</td>
			</tr>
			<tr>
					<td>Internet → DMZ</td>
					<td>–</td>
					<td><strong>nein</strong>, keine Portweiterleitung</td>
					<td>–</td>
			</tr>
	</tbody>
</table>
<p>Drei Details dazu:</p>
<ul>
<li><strong>Die Sperrregel nach innen</strong> zielt nicht auf eine Liste von Interfaces, sondern auf <em>alle privaten Adressbereiche</em> (RFC 1918) über beliebige Interfaces. Lege ich später ein neues VLAN an, ist es automatisch gesperrt. Beim Honeypot musste ich die Regel noch bei jedem neuen Netz anfassen.</li>
<li><strong>Kein Application Control</strong> auf der Internet-Regel. Mein Standardprofil blockt die Kategorie „Proxy“ – und ein Snowflake-Proxy ist nun mal genau das.</li>
<li>Meine <strong>Sperrliste für bekannte Bedrohungen</strong> (inklusive Tor-Knoten aus der Fortinet-Datenbank) gilt auch für die DMZ. Snowflake stört sie nicht: Der Proxy spricht mit dem Broker per HTTPS und mit den Nutzern per WebRTC, nicht mit klassischen Tor-Relays. In den Logs gab es dort nach dem Start keinen einzigen Treffer aus der DMZ.</li>
</ul>
<p>Zum Logging auf der Internet-Regel kommt unten noch eine eigene Geschichte.</p>
<h3 id="schritt-2-eine-kleine-harte-debian-vm">Schritt 2: Eine kleine, harte Debian-VM</h3>
<p>Die VM ist schnell angelegt. Debian per netinst, im Installer nur „SSH-Server“ und „Standard-Systemwerkzeuge“, feste IP, öffentliche DNS-Server.</p>
<p>Kleine Falle: Ich hatte im Installer ein Root-Passwort gesetzt. Dann installiert Debian <strong>kein <code>sudo</code></strong>. Also einmal per <code>su -</code> nachholen und den Root-Zugang danach sperren:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">su -
</span></span><span class="line"><span class="cl">apt install sudo
</span></span><span class="line"><span class="cl">usermod -aG sudo &lt;DEIN_USER&gt;
</span></span><span class="line"><span class="cl">passwd -l root
</span></span></code></pre></div><p>Für SSH gibt es eine eigene Datei mit den Härtungsregeln, nachdem der eigene Schlüssel per <code>ssh-copy-id</code> auf der VM liegt:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo tee /etc/ssh/sshd_config.d/00-hardening.conf &gt;/dev/null <span class="s">&lt;&lt;&#39;EOF&#39;
</span></span></span><span class="line"><span class="cl"><span class="s">PermitRootLogin no
</span></span></span><span class="line"><span class="cl"><span class="s">PasswordAuthentication no
</span></span></span><span class="line"><span class="cl"><span class="s">KbdInteractiveAuthentication no
</span></span></span><span class="line"><span class="cl"><span class="s">AllowUsers &lt;DEIN_USER&gt;
</span></span></span><span class="line"><span class="cl"><span class="s">EOF</span>
</span></span><span class="line"><span class="cl">sudo sshd -t <span class="o">&amp;&amp;</span> sudo systemctl reload ssh
</span></span></code></pre></div><p>Wichtig: Eine zweite SSH-Sitzung offen lassen, bis der Login mit Schlüssel in einer neuen Sitzung klappt. Sonst sperrt man sich bei einem Tippfehler selbst aus.</p>
<p>Dazu <code>unattended-upgrades</code> für Sicherheitsupdates und, bevor Docker draufkommt, ein Proxmox-Snapshot. Dann gibt es einen sauberen Rückweg.</p>
<p>Noch ein Klassiker: Auf meinem Laptop lag für die IP-Adresse der VM noch ein SSH-Host-Key – vom Honeypot. Genauer gesagt vom SSH-<em>Honeypot</em> auf Port 22. SSH hat sich zu Recht lautstark beschwert. <code>ssh-keygen -R 10.0.99.10</code> entfernt den alten Eintrag; den neuen Fingerprint habe ich an der Proxmox-Konsole verglichen, bevor ich ihn akzeptiert habe.</p>
<h3 id="schritt-3-docker--mit-der-lehre-vom-letzten-mal">Schritt 3: Docker – mit der Lehre vom letzten Mal</h3>
<p>Docker kommt aus dem offiziellen Repository. Bevor der erste Container startet, bekommt Docker einen eigenen Adressbereich:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span> <span class="nt">&#34;default-address-pools&#34;</span><span class="p">:</span> <span class="p">[</span> <span class="p">{</span> <span class="nt">&#34;base&#34;</span><span class="p">:</span> <span class="s2">&#34;172.16.0.0/12&#34;</span><span class="p">,</span> <span class="nt">&#34;size&#34;</span><span class="p">:</span> <span class="mi">24</span> <span class="p">}</span> <span class="p">]</span> <span class="p">}</span>
</span></span></code></pre></div><p>Das gehört nach <code>/etc/docker/daemon.json</code>. Beim Honeypot hatte mir Docker mit seinem Standard-Pool aus <code>192.168.x.0/20</code> ein Netz angelegt, das mein Client-Netz überdeckte – mit dem Ergebnis, dass die VM mir nicht mehr antwortete. Das passiert mir kein zweites Mal.</p>
<h3 id="schritt-4-der-proxy-selbst">Schritt 4: Der Proxy selbst</h3>
<p>Der ganze Stack ist eine einzige Compose-Datei, bei mir unter <code>/opt/stacks/snowflake/compose.yaml</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">snowflake-proxy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">thetorproject/snowflake-proxy:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">snowflake-proxy</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">network_mode</span><span class="p">:</span><span class="w"> </span><span class="l">host</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">command</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;-capacity&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;10&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;-summary-interval&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;1h&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">read_only</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">tmpfs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/tmp</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">cap_drop</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">ALL</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">security_opt</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="kc">no</span>-<span class="l">new-privileges:true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">logging</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">driver</span><span class="p">:</span><span class="w"> </span><span class="l">syslog</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">options</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">syslog-address</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;tcp://10.0.20.20:1514&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">syslog-format</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;rfc5424&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">tag</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;snowflake&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">mode</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;non-blocking&#34;</span><span class="w">
</span></span></span></code></pre></div><p>Was die Zeilen tun:</p>
<ul>
<li><strong><code>network_mode: host</code></strong> empfiehlt das Tor-Projekt selbst. WebRTC braucht viele wechselnde UDP-Ports, und hinter Dockers eigenem NAT funktioniert das deutlich schlechter. Weil die VM nichts anderes macht und in einer eigenen DMZ steht, finde ich das hier unbedenklich.</li>
<li><strong><code>-capacity 10</code></strong> begrenzt den Proxy auf zehn gleichzeitige Nutzer. Ohne die Option nimmt er so viele, wie kommen. Ich wollte erst einmal sehen, was das mit meinem Upload macht.</li>
<li><strong><code>-summary-interval 1h</code></strong> sorgt dafür, dass der Proxy einmal pro Stunde eine Zusammenfassung ins Log schreibt. Daraus baue ich später das Dashboard.</li>
<li><strong><code>read_only</code>, <code>cap_drop: ALL</code>, <code>no-new-privileges</code></strong> nehmen dem Container alles, was er nicht braucht. Er läuft damit problemlos.</li>
<li><strong>Logging per Syslog</strong> direkt an Alloy auf meinem Log-Server. <code>non-blocking</code> heißt: Ist der Log-Server mal weg, läuft der Proxy trotzdem weiter.</li>
</ul>
<p>Was hier bewusst <strong>fehlt</strong>: <code>-verbose</code> und <code>-unsafe-logging</code>. Standardmäßig entfernt der Proxy IP-Adressen aus seinen Logs. Mit <code>-unsafe-logging</code> würde er sie mitschreiben. Genau das will ich nicht.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">cd</span> /opt/stacks/snowflake
</span></span><span class="line"><span class="cl">sudo docker compose up -d
</span></span><span class="line"><span class="cl">sudo docker logs -f snowflake-proxy
</span></span></code></pre></div><p>Nach dem Start meldet der Proxy, dass er läuft, und welchen <strong>NAT-Typ</strong> er erkannt hat. Bei mir steht da „moderate“ – ganz ohne Portfreigabe. Das reicht, damit der Broker ihm Nutzer zuteilt.</p>
<h3 id="schritt-5-die-logging-falle--oder-wer-loggt-hier-eigentlich-wen">Schritt 5: Die Logging-Falle – oder: Wer loggt hier eigentlich wen?</h3>
<p>Jetzt der Teil, der mich am meisten beschäftigt hat. Mein Ziel war von Anfang an: <strong>keine Verbindungsdaten</strong>. Ich will sehen, <em>ob</em> und <em>wie viel</em> der Proxy genutzt wird – nicht <em>von wem</em>.</p>
<p>Den Proxy selbst hatte ich dafür richtig eingestellt. Die Firewall aber nicht. Die Internet-Regel der DMZ stand anfangs, wie meine anderen Regeln auch, auf „alle Verbindungen loggen“. Als ich im Grafana nachsah, ob schon etwas passiert, sah ich: jede Menge UDP-Sitzungen von der VM zu fremden IP-Adressen. Das waren die Nutzer. Ihre IP-Adressen landeten also für 30 Tage in meinem Loki.</p>
<p>Für einen Dienst, der Menschen vor Überwachung schützen soll, ist das ziemlich genau das Gegenteil vom Ziel. Also habe ich die Regel umgestellt:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">config firewall policy
</span></span><span class="line"><span class="cl">    edit &lt;POLICY-ID&gt;
</span></span><span class="line"><span class="cl">        set logtraffic utm
</span></span><span class="line"><span class="cl">    next
</span></span><span class="line"><span class="cl">end
</span></span></code></pre></div><p>Mit <code>utm</code> loggt die FortiGate nur noch Sicherheitsereignisse – also wenn Virenscanner, Web-Filter oder IPS anschlagen –, aber keine normalen Verbindungen mehr. Die Sicherheit geht dabei nicht verloren: Die Sperrregel nach innen loggt weiter jeden Versuch, und die Bedrohungs-Sperrliste ebenfalls. Was der Proxy <em>tut</em>, sehe ich ab jetzt nur noch aus seiner eigenen Zusammenfassung. Die Einträge der ersten Minuten laufen nach den 30 Tagen von allein aus Loki heraus.</p>
<p>Mein Learning daraus: <strong>Datensparsamkeit muss man an jeder Stelle der Kette prüfen</strong>, nicht nur in der Anwendung. Die Firewall ist auch ein Logger.</p>
<h3 id="schritt-6-alloy-bringt-die-logs-in-form">Schritt 6: Alloy bringt die Logs in Form</h3>
<p>Die Logs kommen per Syslog bei Alloy an und landen in Loki mit den Labels <code>host=&quot;snowflake&quot;</code> und <code>app=&quot;snowflake&quot;</code>. Ein Schönheitsfehler: Docker schickt alles, was der Container auf <code>stderr</code> schreibt, mit Schweregrad „error“. Der Snowflake-Proxy schreibt sein ganzes Log auf <code>stderr</code>. Im Grafana war also jede harmlose Statuszeile ein roter Fehler.</p>
<p>Abhilfe schafft eine zusätzliche Relabel-Regel in der Alloy-Konfiguration, <em>hinter</em> der Regel, die normalerweise den Schweregrad setzt:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-alloy" data-lang="alloy"><span class="line"><span class="cl"><span class="n">rule</span><span class="w"> </span><span class="o">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="n">source_labels</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="o">[</span><span class="s">&#34;__syslog_message_app_name&#34;</span><span class="o">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="n">regex</span><span class="w">         </span><span class="o">=</span><span class="w"> </span><span class="s">&#34;snowflake&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="n">target_label</span><span class="w">  </span><span class="o">=</span><span class="w"> </span><span class="s">&#34;level&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="n">replacement</span><span class="w">   </span><span class="o">=</span><span class="w"> </span><span class="s">&#34;info&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="o">}</span><span class="w">
</span></span></span></code></pre></div><p>Fallstrick am Rande: Nach dem Neustart von Alloy hat der Syslog-Treiber von Docker die Verbindung nicht von selbst wieder aufgebaut. Erst ein Neustart des Containers brachte die Logs zurück. Wer Alloy neu startet, sollte also kurz prüfen, ob die Docker-Logs noch ankommen.</p>
<h3 id="schritt-7-ein-dashboard-das-zählt-ohne-mitzuschreiben">Schritt 7: Ein Dashboard, das zählt, ohne mitzuschreiben</h3>
<p>Einmal pro Stunde schreibt der Proxy eine Zeile wie diese:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">In the last 1h0m0s, there were 7 completed successful connections. Traffic Relayed ↓ 52310 KB (14.53 KB/s), ↑ 8124 KB (2.26 KB/s).
</span></span></code></pre></div><p>Die Zahlen sind ein Beispiel, das Format stimmt. Mehr braucht es nicht. Mit LogQL lassen sich die Werte aus der Zeile herausziehen und aufsummieren. Die Verbindungen im gewählten Zeitraum:</p>
<pre tabindex="0"><code class="language-logql" data-lang="logql">sum(
  sum_over_time(
    {app=&#34;snowflake&#34;} |= &#34;completed&#34;
      | regexp `there were (?P&lt;conns&gt;\d+) completed( successful)? connections`
      | unwrap conns [$__range]
  )
)
</code></pre><p>Das <code>( successful)?</code> ist kein Zufall: Meine erste Version des Ausdrucks suchte nach „completed connections“ – in der echten Zeile steht aber „completed successful connections“. Mit dem optionalen Teil passt beides.</p>
<p>Nach demselben Muster entstehen die Panels für die weitergeleiteten Daten (<code>↓ (?P&lt;down&gt;\d+) KB</code> und <code>↑ (?P&lt;up&gt;\d+) KB</code>), ein Verlauf pro Stunde und das <strong>Datenvolumen pro Tag</strong>. Dazu kommen ein paar Panels aus den Firewall-Logs: Treffer auf der Sperrregel nach innen und auf der Bedrohungs-Sperrliste. Beide sollten im Normalbetrieb bei null stehen. Steht dort plötzlich etwas, will ich das sehen.</p>
<p>So sieht das Ganze nach knapp einer Woche aus:</p>
<p><img alt="Grafana-Dashboard des Snowflake-Proxys: Verbindungen, weitergeleitete Daten, Verbindungen und Traffic pro Stunde sowie Datenvolumen pro Tag über sieben Tage" loading="lazy" src="/posts/tor-snowflake-proxy-dmz/dashboard-oben.jpg"></p>
<p>Die Firewall-Panels darunter habe ich für den Screenshot weggelassen. Und weil man es im kleinen Bild schlecht erkennt, hier noch einmal das Datenvolumen pro Tag. Jeder Balken endet um Mitternacht, steht also für den Tag davor:</p>
<p><img alt="Balkendiagramm: Datenvolumen pro Tag, grün zu den Nutzern, gelb von den Nutzern; der höchste Tag liegt bei gut 5 GiB" loading="lazy" src="/posts/tor-snowflake-proxy-dmz/datenvolumen-pro-tag.jpg"></p>
<p>Kleine Randnotiz: Beim ersten Versuch begann die y-Achse dieses Panels nicht bei null, sondern bei gut einem Gigabyte. Dadurch sahen die Unterschiede zwischen den Tagen viel dramatischer aus, als sie waren. Wer Balkendiagramme baut: <strong>Soft min 0</strong> setzen.</p>
<h3 id="ein-kurioser-treffer-auf-der-sperrregel">Ein kurioser Treffer auf der Sperrregel</h3>
<p>Ganz bei null blieb die Sperrregel nach innen dann doch nicht. Am ersten Tag zeigte das Dashboard 29 Treffer. Sechs davon war ich selbst: mein Sperrtest beim Einrichten, bei dem ich aus der VM bewusst versucht habe, Dienste im Heimnetz zu erreichen. Die Regel hat gemacht, was sie soll.</p>
<p>Die anderen 23 waren spannender. Alle innerhalb von nicht einmal einer Sekunde, alle UDP, alle an dieselbe private Adresse aus dem Bereich <code>172.17.x.x</code> – eine Adresse, die es bei mir gar nicht gibt. Vier verschiedene lokale Ports, Runde um Runde im Abstand von etwa 200 Millisekunden. Und das Interessanteste: Laut Firewall-Log wären die Pakete ohne die Sperrregel <strong>ins Internet</strong> gegangen, nicht in mein Heimnetz. Das hatte ich im ersten Moment falsch eingeschätzt.</p>
<p>Meine Vermutung, ausdrücklich <strong>nicht bewiesen</strong>: Das waren Verbindungstests von WebRTC. Bevor zwei Gegenstellen per WebRTC miteinander reden, tauschen sie mögliche Adressen aus (<strong>ICE-Kandidaten</strong>, ICE = Interactive Connectivity Establishment) und probieren sie der Reihe nach durch. Schickt eine Gegenseite dabei auch eine private Adresse mit – <code>172.17.x.x</code> ist zum Beispiel das Standardnetz von Docker –, versucht mein Proxy eben auch die. Woher die Adresse genau kam, weiß ich nicht. Mitgeschnitten habe ich bewusst nichts: Das wären genau die Verbindungsdaten, die ich nicht haben will.</p>
<p>Für mich ist das trotzdem das beste Argument für die Sperrregel. Diesmal lag die fremde Adresse zufällig in einem Bereich, den ich nicht nutze. Hätte die Gegenseite eine <code>192.168.x.x</code> geschickt, die zufällig in meinem Heimnetz existiert, hätte die Firewall ohne die Regel brav nach innen geroutet.</p>
<h2 id="was-kommt-rein-was-geht-raus">Was kommt rein, was geht raus?</h2>
<p>Gestartet habe ich den Proxy in der Nacht zum 1. November. Die erste Zusammenfassung eine Stunde später war ernüchternd: null Verbindungen, ein paar Hundert Kilobyte Rauschen. Ich hatte schon den Verdacht, irgendwas falsch gemacht zu haben. Hatte ich nicht – nach 18 Stunden standen 84 Verbindungen und rund ein Gigabyte im Zähler.</p>
<p>Die erste Woche, gezählt nach deutscher Zeit und aufsummiert aus den stündlichen Zusammenfassungen:</p>
<table>
	<thead>
			<tr>
					<th>Tag</th>
					<th>Verbindungen</th>
					<th>↓ weitergeleitet</th>
					<th>↑ weitergeleitet</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>01.11.</td>
					<td>112</td>
					<td>1,5 GB</td>
					<td>248 MB</td>
			</tr>
			<tr>
					<td>02.11.</td>
					<td>116</td>
					<td>4,5 GB</td>
					<td>464 MB</td>
			</tr>
			<tr>
					<td>03.11.</td>
					<td>124</td>
					<td>2,7 GB</td>
					<td>287 MB</td>
			</tr>
			<tr>
					<td>04.11.</td>
					<td>93</td>
					<td>3,0 GB</td>
					<td>365 MB</td>
			</tr>
			<tr>
					<td>05.11.</td>
					<td>173</td>
					<td>3,2 GB</td>
					<td>336 MB</td>
			</tr>
			<tr>
					<td>06.11. (bis 17 Uhr)</td>
					<td>56</td>
					<td>1,7 GB</td>
					<td>172 MB</td>
			</tr>
			<tr>
					<td><strong>Summe</strong></td>
					<td><strong>674</strong></td>
					<td><strong>16,4 GB</strong></td>
					<td><strong>1,8 GB</strong></td>
			</tr>
	</tbody>
</table>
<p>Was mir daran auffällt:</p>
<ul>
<li><strong>Die Nachfrage ist erstaunlich gleichmäßig.</strong> Meist vier bis sechs Verbindungen pro Stunde, Tag und Nacht. Nur ganz vereinzelt gab es eine Stunde ohne Verbindung, und einmal schoss es auf 41 Verbindungen in einer Stunde hoch.</li>
<li><strong>Das Volumen schwankt stärker als die Verbindungen.</strong> Am 2. November floss bei praktisch gleicher Verbindungszahl dreimal so viel wie am Tag davor. Warum, kann ich nicht sagen – und genau so soll es sein: Was die Leute über meinen Proxy machen, sehe ich nicht.</li>
<li><strong>Neun zu eins.</strong> Grob neunmal so viel geht Richtung Nutzer (↓) wie zurück. Das passt zum typischen Surfen: kleine Anfragen raus, Webseiten und Videos rein.</li>
<li><strong>Die Kapazität von zehn</strong> kann ich aus den Zusammenfassungen nicht ablesen. Sie zählen nur abgeschlossene Verbindungen pro Stunde, nicht, wie viele gleichzeitig offen waren.</li>
</ul>
<p>Und im Alltag? Merke ich davon nichts.</p>
<p>Was man beim Datenvolumen wissen sollte: Alles, was der Proxy weiterleitet, läuft <strong>zweimal</strong> über meine Leitung – einmal herein und einmal hinaus. Die Summe aus ↓ und ↑ in der Zusammenfassung taucht also sowohl im Download als auch im Upload meines Anschlusses auf. Bei mir sind das gut 3 GB pro Tag in jede Richtung, im Schnitt also etwa 0,3 Mbit/s – für einen normalen Anschluss kaum der Rede wert. Die Begrenzung auf zehn gleichzeitige Nutzer hält auch die Spitzen im Rahmen.</p>
<p>Zum Ressourcenverbrauch: Proxmox zeigte für die VM zeitweise über 100 % RAM-Auslastung an. Das sah dramatischer aus, als es war. Ohne Ballooning zeigt Proxmox die Sicht des Hosts inklusive Cache und QEMU-Overhead. In der VM selbst waren knapp 500 MB frei, der Proxy braucht rund 140 MB, hauptsächlich für seine GeoIP-Tabellen.</p>
<h2 id="stolperfallen--learnings">Stolperfallen &amp; Learnings</h2>
<p><strong>Die Firewall ist auch ein Logger.</strong> Ich hatte am Proxy alles richtig gemacht und trotzdem Nutzer-IPs gespeichert – über die Firewall. Wer bewusst keine Verbindungsdaten will, muss die ganze Kette ansehen: Anwendung, Container-Runtime, Firewall, Log-Server.</p>
<p><strong>Application Control und Proxys vertragen sich nicht.</strong> Ein Profil, das die Kategorie „Proxy“ blockt, blockt auch einen Proxy, den man absichtlich betreibt. Für die DMZ-Regel gibt es deshalb ein eigenes Sicherheitsprofil ohne App Control.</p>
<p><strong>Docker und Syslog: stderr ist nicht gleich Fehler.</strong> Der Syslog-Treiber stuft <code>stderr</code> pauschal als „error“ ein. Das lässt sich in Alloy geradeziehen, sollte man aber wissen, bevor man Alarme auf „error“ baut.</p>
<p><strong>Docker-Adresspools vor dem ersten Start setzen.</strong> Das habe ich beim Honeypot schmerzhaft gelernt und diesmal gleich richtig gemacht.</p>
<p><strong>Alte Host-Keys erzählen Geschichten.</strong> Die SSH-Warnung beim ersten Login war berechtigt – unter derselben IP wohnte vorher ein Honeypot. Nicht einfach wegklicken, sondern Fingerprint prüfen.</p>
<p><strong>Eine Sperrregel nach innen lohnt sich auch dann, wenn man glaubt, sie nicht zu brauchen.</strong> WebRTC probiert Adressen aus, die ihm die Gegenseite schickt – auch private. Ohne Regel entscheidet allein das Routing, wo so ein Paket landet.</p>
<p><strong>Geduld.</strong> Mit <code>-summary-interval 1h</code> kommt die erste Zusammenfassung frühestens eine Stunde nach dem Start. Und wenn man den Container zwischendurch neu startet, fängt die Stunde wieder von vorn an. Ich bin nicht gut im Warten.</p>
<h2 id="ein-wort-zum-rechtlichen">Ein Wort zum Rechtlichen</h2>
<p>Ich bin kein Anwalt, deshalb nur meine Einschätzung: Ein Snowflake-Proxy ist kein Exit-Knoten. Von meinem Anschluss gehen also keine Anfragen an fremde Webseiten aus, für die ich Ärger bekommen könnte. Mein Provider sieht lediglich, dass ich viele WebRTC-Verbindungen habe. Wer unsicher ist, wirft vorher einen Blick in die Hinweise des Tor-Projekts und in die Bedingungen des eigenen Anschlusses.</p>
<h2 id="fazit">Fazit</h2>
<p>Für eine Handvoll Zeilen Compose und eine kleine VM ist das ein ziemlich gutes Verhältnis von Aufwand und Nutzen. Mein Anschluss gibt ein bisschen Bandbreite ab, und irgendwo kommt jemand an Informationen, die ihm sonst verwehrt wären. Die DMZ hat nach dem Honeypot eine Aufgabe, die keinen Ärger macht, und ich habe nebenbei gelernt, dass Datensparsamkeit nicht in der Anwendung endet.</p>
<h2 id="was-als-nächstes-kommt">Was als Nächstes kommt</h2>
<ul>
<li><strong>Updates regeln:</strong> Das Image regelmäßig aktualisieren, ob per Hand mit <code>docker compose pull</code> oder automatisch nur für diesen einen Container.</li>
<li><strong>Metriken statt Logzeilen:</strong> Der Proxy kann mit <code>-metrics</code> auch Prometheus-Metriken liefern. Dafür bräuchte ich aber erst einmal einen Metrik-Speicher im Homelab.</li>
<li><strong>Kapazität anpassen:</strong> Wenn die Leitung es hergibt, darf der Proxy mehr als zehn Nutzer gleichzeitig bedienen.</li>
</ul>
]]></content:encoded></item></channel></rss>