<?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>Werbeblocker on Kabelbruch</title><link>https://kabelbruch.de/tags/werbeblocker/</link><description>Recent content in Werbeblocker 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>Wed, 04 Nov 2026 21:15:00 +0100</lastBuildDate><atom:link href="https://kabelbruch.de/tags/werbeblocker/index.xml" rel="self" type="application/rss+xml"/><item><title>Mein eigener Artikel stand auf der Sperrliste</title><link>https://kabelbruch.de/posts/werbeblocker-sperrt-eigenen-artikel/</link><pubDate>Wed, 04 Nov 2026 21:15:00 +0100</pubDate><guid>https://kabelbruch.de/posts/werbeblocker-sperrt-eigenen-artikel/</guid><description>Ein Werbeblocker sperrte genau einen Artikel auf Kabelbruch. Schuld war kein Tracker, sondern ein Wort im Slug. Wie ich die Regel gefunden habe.</description><content:encoded><![CDATA[<p>Ein Leser hat mir auf Mastodon geschrieben. Er fand den Blog interessant, aber ein Artikel ließ sich bei ihm nicht öffnen. Alle anderen schon. Safari meldete nur, die Seite werde „von einer Inhaltssperre blockiert“. Er schrieb selbst, das sei wohl eher ein Fun-Fact. Etwas später kam der Nachtrag, dass er unter macOS mit Safari und dem Werbeblocker <strong>wBlock</strong> unterwegs ist.</p>
<p>Der betroffene Artikel war ausgerechnet der über <a href="/posts/besucherstatistik-matomo-log-import/">Besucherstatistik ohne Cookie-Banner</a>. Also genau der Artikel, in dem ich erkläre, warum hier kein Tracking-Skript läuft.</p>
<p>Mein erster Gedanke: Das darf doch nicht wahr sein. Ausgerechnet ein Artikel über die Erfassung von Besucherdaten, bei dem ich extrem darauf achte, datensparsam zu sein, wird geblockt.</p>
<h2 id="mein-erster-verdacht">Mein erster Verdacht</h2>
<p>Der Artikel war der einzige mit „matomo“ in der Adresse. Viele Blocklisten haben Regeln gegen Matomo, weil Matomo meistens als Tracking-Skript im Browser läuft. Bei mir läuft es zwar nur auf dem Server und wertet Logdateien aus, aber das weiß eine Filterliste nicht. Das klang plausibel, und so habe ich es auch geantwortet.</p>
<p>Es war falsch. Aber dazu brauchte ich erst die Liste der aktiven Filter. Der Leser hatte diese sieben aktiv:</p>
<ul>
<li>AdGuard Base Filter</li>
<li>AdGuard Tracking Protection Filter</li>
<li>AdGuard URL Tracking Protection Filter</li>
<li>Actually Legitimate URL Shortener Tool</li>
<li>Peter Lowe&rsquo;s Blocklist</li>
<li>AdGuard Cookie Notices</li>
<li>AdGuard German Filter</li>
</ul>
<h2 id="suchen-in-300000-zeilen">Suchen in 300.000 Zeilen</h2>
<p>Die AdGuard-Listen liegen offen auf GitHub im Repo <a href="https://github.com/AdguardTeam/FiltersRegistry" target="_blank" rel="noopener noreferrer">AdguardTeam/FiltersRegistry</a>, auch in der Fassung für Safari. Zusammen sind das gut 300.000 Zeilen. Ich habe sie heruntergeladen und nach den Wörtern aus der Adresse durchsucht: <code>matomo</code>, <code>log-import</code>, <code>besucherstatistik</code>, <code>cookie-banner</code>.</p>
<p>Bei <code>matomo</code> kam nichts, was eine Seite sperrt. Die Treffer waren nur Regeln, die auf bestimmten fremden Seiten Cookie-Dialoge wegklicken. Bei <code>cookie-banner</code> dagegen stand in der Liste <strong>AdGuard Cookie Notices</strong> diese Zeile:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">-cookie-banner-$domain=~killthecookiebanner.eu
</span></span></code></pre></div><p>Kurz übersetzt:</p>
<ul>
<li><code>-cookie-banner-</code> ist das Muster. Es hat keinen Anker am Anfang oder Ende, passt also auf <strong>jede</strong> Adresse, in der diese Zeichenfolge irgendwo vorkommt.</li>
<li><code>$domain=~killthecookiebanner.eu</code> heißt: überall, außer auf dieser einen Domain. Die Tilde ist ein „nicht“.</li>
</ul>
<p>Gedacht ist die Regel für Dateien wie <code>/js/site-cookie-banner-v2.js</code>. Mein Slug, also der Teil der Adresse, der den Artikel benennt, sah aber so aus:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">/posts/besucherstatistik-ohne-cookie-banner-matomo-log-import/
</span></span></code></pre></div><p>Da steht es: <code>-ohne-cookie-banner-matomo</code>. Bindestrich, cookie, Bindestrich, banner, Bindestrich. Die Regel hat keine Einschränkung auf Skripte oder Bilder, und wBlock hat damit in Safari gleich die ganze Seite gesperrt. Kein anderer Artikel hat diese Zeichenfolge in der Adresse, deshalb ging alles andere.</p>
<p>In Safari nachgestellt habe ich das nicht. Ob andere Werbeblocker mit derselben Liste auch das ganze Dokument sperren oder nur Dateien, habe ich ebenfalls nicht geprüft.</p>
<p>Die Ironie gefällt mir trotzdem: Ein Filter gegen Cookie-Banner sperrt einen Artikel darüber, wie man ohne Cookie-Banner auskommt.</p>
<h2 id="die-lösung-neue-adresse">Die Lösung: neue Adresse</h2>
<p>Der Inhalt war nie das Problem, nur die Adresse. Filterlisten wie diese sehen den Text eines Artikels gar nicht, sie prüfen URLs. Also bekam der Artikel einen neuen Slug ohne die verdächtige Zeichenfolge. Im Front Matter sieht das so aus:</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">slug</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;besucherstatistik-matomo-log-import&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">aliases</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;/posts/besucherstatistik-ohne-cookie-banner-matomo-log-import/&#34;</span><span class="p">]</span><span class="w">
</span></span></span></code></pre></div><p>Mit <code>aliases</code> legt Hugo unter der alten Adresse eine kleine Seite an, die per Meta-Refresh auf die neue weiterleitet. Die Seite enthält kein JavaScript, an der Content-Security-Policy ändert sich also nichts. Links, die schon im Umlauf sind, funktionieren damit weiter.</p>
<p>Mit einer Einschränkung: Wer die Filterliste aktiv hat, bleibt auf der alten Adresse hängen. Die Weiterleitungsseite liegt ja genau dort, wo die Regel greift. Diesen Lesern hilft nur der neue Link.</p>
<p>Drei andere Artikel haben auf den Matomo-Artikel verlinkt. Die zeigen jetzt direkt auf die neue Adresse statt über die Weiterleitung.</p>
<p>Eine Sorge hatte ich noch: Meine Pipeline <a href="/posts/blog-tootet-selbst-mastodon-hugo/">kündigt neue Artikel selbst auf Mastodon an</a>, und sie merkt sich das über die Adresse. Für sie war der Matomo-Artikel unter der neuen Adresse also ein „neuer“ Artikel. Getootet hat sie trotzdem nicht, weil sie nur Artikel ankündigt, die höchstens drei Tage alt sind. Da hat mich eine Schutzregel vor einem doppelten Toot bewahrt, an die ich bei diesem Problem gar nicht gedacht hatte.</p>
<p>Kurz nach der Umstellung kam die Rückmeldung vom Leser: Die Seite lässt sich jetzt „ohne Murren“ öffnen.</p>
<h2 id="ein-grober-check-für-neue-slugs">Ein grober Check für neue Slugs</h2>
<p>Damit mir das nicht noch einmal passiert, habe ich ein kleines Skript, das eine Adresse gegen die Listen prüft. Es ist kein Nachbau eines Werbeblockers. Es kennt nur einfache URL-Regeln, ignoriert Ausnahmen und die meisten Optionen. Für die Frage „Steht in meinem Slug etwas, worauf eine Liste anspringt?“ reicht es.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="ch">#!/usr/bin/env python3</span>
</span></span><span class="line"><span class="cl"><span class="s2">&#34;&#34;&#34;Grober Check: Welche einfache Filterregel passt auf eine URL?
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">Kein vollständiger Adblocker-Nachbau. Er kennt nur einfache URL-Regeln,
</span></span></span><span class="line"><span class="cl"><span class="s2">ignoriert Ausnahmen (@@) und die meisten Optionen.
</span></span></span><span class="line"><span class="cl"><span class="s2">&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">re</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">sys</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">urllib.request</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">BASE</span> <span class="o">=</span> <span class="s2">&#34;https://raw.githubusercontent.com/AdguardTeam/FiltersRegistry/master/platforms/extension/safari/filters/&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">LISTS</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;AdGuard Base&#34;</span><span class="p">:</span> <span class="s2">&#34;2.txt&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;AdGuard Tracking Protection&#34;</span><span class="p">:</span> <span class="s2">&#34;3.txt&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;AdGuard Cookie Notices&#34;</span><span class="p">:</span> <span class="s2">&#34;18.txt&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;AdGuard German&#34;</span><span class="p">:</span> <span class="s2">&#34;6.txt&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;Peter Lowe&#39;s Blocklist&#34;</span><span class="p">:</span> <span class="s2">&#34;204.txt&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="n">SKIP_OPTIONS</span> <span class="o">=</span> <span class="p">(</span><span class="s2">&#34;third-party&#34;</span><span class="p">,</span> <span class="s2">&#34;script&#34;</span><span class="p">,</span> <span class="s2">&#34;image&#34;</span><span class="p">,</span> <span class="s2">&#34;stylesheet&#34;</span><span class="p">,</span> <span class="s2">&#34;xmlhttprequest&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="s2">&#34;font&#34;</span><span class="p">,</span> <span class="s2">&#34;media&#34;</span><span class="p">,</span> <span class="s2">&#34;subdocument&#34;</span><span class="p">,</span> <span class="s2">&#34;permissions&#34;</span><span class="p">,</span> <span class="s2">&#34;removeparam&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">to_regex</span><span class="p">(</span><span class="n">pattern</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">pattern</span><span class="o">.</span><span class="n">startswith</span><span class="p">(</span><span class="s2">&#34;||&#34;</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="n">rx</span><span class="p">,</span> <span class="n">pattern</span> <span class="o">=</span> <span class="sa">r</span><span class="s2">&#34;^[a-z]+://([^/]*\.)?&#34;</span><span class="p">,</span> <span class="n">pattern</span><span class="p">[</span><span class="mi">2</span><span class="p">:]</span>
</span></span><span class="line"><span class="cl">    <span class="k">elif</span> <span class="n">pattern</span><span class="o">.</span><span class="n">startswith</span><span class="p">(</span><span class="s2">&#34;|&#34;</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="n">rx</span><span class="p">,</span> <span class="n">pattern</span> <span class="o">=</span> <span class="s2">&#34;^&#34;</span><span class="p">,</span> <span class="n">pattern</span><span class="p">[</span><span class="mi">1</span><span class="p">:]</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">rx</span> <span class="o">=</span> <span class="s2">&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">c</span> <span class="ow">in</span> <span class="n">pattern</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">c</span> <span class="o">==</span> <span class="s2">&#34;*&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">rx</span> <span class="o">+=</span> <span class="s2">&#34;.*&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="k">elif</span> <span class="n">c</span> <span class="o">==</span> <span class="s2">&#34;^&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">rx</span> <span class="o">+=</span> <span class="sa">r</span><span class="s2">&#34;([^\w.%-]|$)&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">rx</span> <span class="o">+=</span> <span class="n">re</span><span class="o">.</span><span class="n">escape</span><span class="p">(</span><span class="n">c</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">rx</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">check</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="n">name</span><span class="p">,</span> <span class="n">text</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">line</span> <span class="ow">in</span> <span class="n">text</span><span class="o">.</span><span class="n">splitlines</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">        <span class="n">line</span> <span class="o">=</span> <span class="n">line</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># Kommentare, Ausnahmen, Regex-Regeln und kosmetische Regeln überspringen</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="ow">not</span> <span class="n">line</span> <span class="ow">or</span> <span class="n">line</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="ow">in</span> <span class="s2">&#34;![@/&#34;</span> <span class="ow">or</span> <span class="s2">&#34;#&#34;</span> <span class="ow">in</span> <span class="n">line</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="k">continue</span>
</span></span><span class="line"><span class="cl">        <span class="n">pattern</span><span class="p">,</span> <span class="n">_</span><span class="p">,</span> <span class="n">options</span> <span class="o">=</span> <span class="n">line</span><span class="o">.</span><span class="n">partition</span><span class="p">(</span><span class="s2">&#34;$&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="nb">any</span><span class="p">(</span><span class="n">o</span><span class="o">.</span><span class="n">lstrip</span><span class="p">(</span><span class="s2">&#34;~&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">startswith</span><span class="p">(</span><span class="n">SKIP_OPTIONS</span><span class="p">)</span> <span class="k">for</span> <span class="n">o</span> <span class="ow">in</span> <span class="n">options</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">&#34;,&#34;</span><span class="p">)</span> <span class="k">if</span> <span class="n">o</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">            <span class="k">continue</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="s2">&#34;domain=&#34;</span> <span class="ow">in</span> <span class="n">options</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">re</span><span class="o">.</span><span class="n">search</span><span class="p">(</span><span class="sa">r</span><span class="s2">&#34;domain=~&#34;</span><span class="p">,</span> <span class="n">options</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">            <span class="k">continue</span>  <span class="c1"># Regel gilt nur für bestimmte fremde Domains</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">pattern</span><span class="p">)</span> <span class="o">&gt;=</span> <span class="mi">4</span> <span class="ow">and</span> <span class="n">re</span><span class="o">.</span><span class="n">search</span><span class="p">(</span><span class="n">to_regex</span><span class="p">(</span><span class="n">pattern</span><span class="p">),</span> <span class="n">url</span><span class="p">,</span> <span class="n">re</span><span class="o">.</span><span class="n">I</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">            <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">name</span><span class="si">}</span><span class="s2">: </span><span class="si">{</span><span class="n">line</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">for</span> <span class="n">url</span> <span class="ow">in</span> <span class="n">sys</span><span class="o">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">:]:</span>
</span></span><span class="line"><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;== </span><span class="si">{</span><span class="n">url</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">name</span><span class="p">,</span> <span class="n">file</span> <span class="ow">in</span> <span class="n">LISTS</span><span class="o">.</span><span class="n">items</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">        <span class="k">with</span> <span class="n">urllib</span><span class="o">.</span><span class="n">request</span><span class="o">.</span><span class="n">urlopen</span><span class="p">(</span><span class="n">BASE</span> <span class="o">+</span> <span class="n">file</span><span class="p">)</span> <span class="k">as</span> <span class="n">r</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">check</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="n">name</span><span class="p">,</span> <span class="n">r</span><span class="o">.</span><span class="n">read</span><span class="p">()</span><span class="o">.</span><span class="n">decode</span><span class="p">(</span><span class="s2">&#34;utf-8&#34;</span><span class="p">,</span> <span class="s2">&#34;ignore&#34;</span><span class="p">))</span>
</span></span></code></pre></div><p>Aufgerufen mit der alten und der neuen Adresse:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">$</span> python3 slug-check.py <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="go">    https://example.com/posts/besucherstatistik-ohne-cookie-banner-matomo-log-import/ \
</span></span></span><span class="line"><span class="cl"><span class="go">    https://example.com/posts/besucherstatistik-matomo-log-import/
</span></span></span><span class="line"><span class="cl"><span class="go">== https://example.com/posts/besucherstatistik-ohne-cookie-banner-matomo-log-import/
</span></span></span><span class="line"><span class="cl"><span class="go">AdGuard Cookie Notices: -cookie-banner-$domain=~killthecookiebanner.eu
</span></span></span><span class="line"><span class="cl"><span class="go">== https://example.com/posts/besucherstatistik-matomo-log-import/
</span></span></span></code></pre></div><p>Die alte Adresse trifft genau eine Regel, die neue keine. Die Listen ändern sich ständig, das Ergebnis gilt also nur für den Tag, an dem man prüft. Bei mir war das der 4. November 2026.</p>
<h2 id="was-ich-daraus-mitnehme">Was ich daraus mitnehme</h2>
<p><strong>Der erste Verdacht war plausibel und trotzdem falsch.</strong> „matomo in der Adresse“ klang so logisch, dass ich es fast nicht geprüft hätte. Erst die Liste der aktiven Filter und ein stumpfes Durchsuchen haben die echte Ursache gezeigt.</p>
<p><strong>Auch Adressen müssen durch Filter.</strong> Ich achte bei Adressen auf Lesbarkeit und Suchbegriffe. Dass sie auch durch Filterlisten müssen, die nur Zeichenketten vergleichen, hatte ich nicht auf dem Schirm. Wörter wie <code>cookie-banner</code>, <code>tracking</code> oder <code>ads</code> mit Bindestrichen drumherum sind in einer Adresse riskant.</p>
<p><strong>Meldungen von Lesern sind Gold wert.</strong> Ohne die Nachricht hätte ich das nie bemerkt. In meinen Logs taucht ein gesperrter Abruf gar nicht auf, weil der Browser die Anfrage nie abschickt. Danke an den Leser, der sich die Mühe gemacht hat.</p>
<h2 id="wie-es-weitergeht">Wie es weitergeht</h2>
<p>Bei AdGuard melde ich das nicht. Die Regel ist breit, aber ich habe meine Seite umgebaut und achte in Zukunft selbst darauf, was in meinen Adressen steht.</p>
<p>Dafür überlege ich, den Slug-Check als Schritt in die Pipeline zu hängen, so wie die Prüfung der CSP-Hashes. Dann würde mir ein neuer Artikel mit verdächtiger Adresse schon vor der Veröffentlichung auffallen und nicht erst, wenn jemand auf Mastodon nachfragt.</p>
]]></content:encoded></item><item><title>DNS im Heimnetz: zwei Technitium-Server und eine Firewall, die aufpasst</title><link>https://kabelbruch.de/posts/dns-heimnetz-technitium-fortigate/</link><pubDate>Sun, 01 Nov 2026 22:50:00 +0100</pubDate><guid>https://kabelbruch.de/posts/dns-heimnetz-technitium-fortigate/</guid><description>Zwei Technitium-DNS-Server mit Filterlisten und DoT, eine FortiGate, die kein DNS an ihnen vorbeilässt – und was ich dabei übersehen habe.</description><content:encoded><![CDATA[<p>Jedes Gerät im Haus fragt DNS – und manche fragen, wen sie wollen. Bei mir antworten zwei eigene Technitium-Server, die Firewall sorgt dafür, dass niemand an ihnen vorbeikommt, und ein Feature habe ich dabei glatt übersehen.</p>
<h2 id="worum-gehts">Worum geht&rsquo;s?</h2>
<p><strong>DNS</strong> ist das Telefonbuch des Internets: Ein Gerät fragt „Welche IP hat <code>example.com</code>?“ und bekommt eine Adresse zurück. Wer diese Fragen beantwortet, sieht ziemlich genau, was im Haus los ist – und kann entscheiden, welche Antworten es überhaupt gibt.</p>
<p>Bisher hat das bei mir der alte Router erledigt. Ein Pi-hole-Container lief früher auch mal, ist aber seit September abgeschaltet. Mit dem Umstieg auf eine FortiGate als Firewall wollte ich DNS sauber neu aufziehen. Meine Wunschliste:</p>
<ul>
<li><strong>Eigene Namen fürs Homelab</strong>, damit ich Dienste unter <code>dienst.home.example.com</code> erreiche statt unter einer IP.</li>
<li><strong>Werbung und Tracker filtern</strong>, für alle Geräte im Haus, ohne dass ich auf jedem etwas installieren muss.</li>
<li><strong>Verschlüsselt nach außen</strong>, damit mein Provider nicht mitlesen kann, welche Namen ich auflöse.</li>
<li><strong>Ausfallsicher</strong>: Wenn ein DNS-Server ausfällt, geht im Haus gefühlt das Internet aus. Also zwei.</li>
<li><strong>Keine Schlupflöcher</strong>: Geräte, die sich ihren eigenen DNS-Server aussuchen, sollen trotzdem bei mir landen.</li>
</ul>
<h2 id="voraussetzungen">Voraussetzungen</h2>
<ul>
<li><strong>Proxmox VE</strong> als Hypervisor</li>
<li><strong>Technitium DNS Server</strong> 15.6.1 (eingerichtet mit 15.6), in zwei LXC-Containern mit Debian 12, je 1 vCPU und 512 MB RAM</li>
<li><strong>FortiGate</strong> als Firewall mit mehreren VLANs (getrennten Netzen) für Server, Clients, IoT-Geräte, Gäste und so weiter</li>
<li><strong>Quad9</strong> als Upstream-Resolver per DNS-over-TLS</li>
<li><strong>Nginx Proxy Manager</strong> als Reverse Proxy für die internen Dienste</li>
</ul>
<p>Warum Technitium und nicht Pi-hole oder AdGuard Home? Technitium ist ein vollwertiger DNS-Server: Er kann echte Zonen verwalten, Zonentransfers, DNS-over-TLS nach außen, Filterlisten – alles in einem. Bei Pi-hole und AdGuard Home fehlten mir genau diese Einstellmöglichkeiten. Und der Abgleich zwischen zwei Servern war bei beiden nicht optimal, das läuft dort meist über zusätzliche Werkzeuge.</p>
<h2 id="der-aufbau">Der Aufbau</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">             Clients, IoT, Gäste, Server …
</span></span><span class="line"><span class="cl">                        │  DNS (Port 53)
</span></span><span class="line"><span class="cl">          ┌─────────────┴─────────────┐
</span></span><span class="line"><span class="cl">          ▼                           ▼
</span></span><span class="line"><span class="cl">   dns-01 (Primary)  ── Zonentransfer ──▶  dns-02 (Secondary)
</span></span><span class="line"><span class="cl">          │                           │
</span></span><span class="line"><span class="cl">          └──────── DNS-over-TLS ─────┘
</span></span><span class="line"><span class="cl">                        ▼
</span></span><span class="line"><span class="cl">                      Quad9
</span></span></code></pre></div><p>Beide Server laufen als LXC-Container auf Proxmox, mit fester IP im Server-Netz (im Beispiel <code>10.0.10.53</code> und <code>10.0.10.54</code>), und starten beim Booten des Hosts als Erste. Der DHCP-Server der FortiGate verteilt in allen Netzen <strong>beide</strong> Adressen als DNS-Server. Fällt einer aus, fragen die Geräte einfach den anderen.</p>
<h3 id="die-interne-zone">Die interne Zone</h3>
<p>Auf dns-01 liegt die Zone <code>home.example.com</code> als <strong>Primary</strong>, also als Original. Der wichtigste Eintrag ist ein Wildcard:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">*.home.example.com.   300   IN   A   10.0.10.80
</span></span></code></pre></div><p>Jeder Name unter <code>home.example.com</code> zeigt damit auf den Reverse Proxy, der die Anfrage an den richtigen Dienst weiterreicht. Für einen neuen Dienst muss ich nur im Proxy etwas eintragen, nicht im DNS. Ein paar Namen, die nicht über den Proxy laufen, stehen zusätzlich als eigene A-Einträge drin.</p>
<p>dns-02 hält dieselbe Zone als <strong>Secondary</strong>: Er holt sie sich per <strong>Zonentransfer</strong> von dns-01. Damit Änderungen nicht erst beim nächsten regulären Abgleich ankommen, schickt dns-01 bei jeder Änderung ein <strong>Notify</strong> – eine kurze Nachricht „Hey, es gibt was Neues“, worauf dns-02 sofort nachlädt.</p>
<h3 id="filterlisten">Filterlisten</h3>
<p>Auf beiden Servern sind dieselben Listen eingetragen:</p>
<ul>
<li><strong>HaGeZi Multi PRO</strong>: blockt Werbung, Tracker und bekannte Schaddomains. Die Liste wird alle 24 Stunden aktualisiert.</li>
<li><strong>HaGeZi „Encrypted DNS Bypass“</strong>: blockt bekannte Anbieter von DNS-over-HTTPS (DoH). Dazu gleich mehr.</li>
<li><strong><code>use-application-dns.net</code></strong>: Das ist eine „Kanarienvogel-Domain“. Firefox fragt sie ab, bevor er von sich aus DoH einschaltet. Bekommt er keine Antwort, lässt er es bleiben und nutzt das DNS des Netzes.</li>
</ul>
<h3 id="verschlüsselt-nach-außen">Verschlüsselt nach außen</h3>
<p>Was Technitium nicht selbst weiß, fragt er bei <strong>Quad9</strong> nach – und zwar per <strong>DNS-over-TLS</strong> (DoT). DoT ist normales DNS, nur in einem verschlüsselten Tunnel über Port 853. Mein Provider sieht dann nur noch, dass ich mit Quad9 rede, aber nicht mehr, worüber.</p>
<p>Wie viel da zusammenkommt, zeigt das Dashboard von dns-01: In 24 Stunden kamen rund <strong>92 000 Anfragen</strong> von 40 Geräten an. Knapp <strong>16 Prozent</strong> davon hat Technitium geblockt, also ungefähr jede sechste. Einen Teil der Anfragen beantwortet außerdem dns-02, die echte Gesamtzahl im Haus liegt also noch etwas höher.</p>
<p><img alt="Das Technitium-Dashboard von dns-01 über 24 Stunden: Anfragen insgesamt, beantwortet, aus dem Cache und geblockt" loading="lazy" src="/posts/dns-heimnetz-technitium-fortigate/technitium-dashboard.png"></p>
<h2 id="die-firewall-passt-auf">Die Firewall passt auf</h2>
<p>Eigene DNS-Server bringen wenig, wenn Geräte einfach woanders fragen. Und das tun erstaunlich viele.</p>
<h3 id="regel-1-dns-nur-an-die-eigenen-server">Regel 1: DNS nur an die eigenen Server</h3>
<p>Auf der FortiGate gibt es zwei Regeln, sinngemäß:</p>
<table>
	<thead>
			<tr>
					<th>Von</th>
					<th>Nach</th>
					<th>Was</th>
					<th>Erlaubt?</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Clients, IoT, Gäste, Server …</td>
					<td>die beiden DNS-Server</td>
					<td>DNS</td>
					<td>ja</td>
			</tr>
			<tr>
					<td>dieselben Netze, außer den DNS-Servern selbst</td>
					<td>Internet</td>
					<td>DNS (53) und DoT (853)</td>
					<td><strong>nein</strong></td>
			</tr>
	</tbody>
</table>
<p>Nur dns-01 und dns-02 dürfen also selbst nach draußen. Alle anderen Geräte kommen mit DNS nur noch bis zu ihnen.</p>
<h3 id="regel-2-wer-stur-google-fragt-landet-trotzdem-bei-mir">Regel 2: Wer stur Google fragt, landet trotzdem bei mir</h3>
<p>Ein paar Geräte halten sich nicht an den DNS-Server, den ihnen DHCP mitgibt. Bei mir waren das vor allem die <strong>Echo-Lautsprecher und der Fire TV</strong>: Die fragen fest eingestellt bei Google (<code>8.8.8.8</code> und <code>8.8.4.4</code>). Mit Regel 1 würden sie einfach kein DNS mehr bekommen und nicht mehr funktionieren.</p>
<p>Die Lösung auf der FortiGate ist eine <strong>Umleitung</strong> (dort „VIP“ genannt): Anfragen an Googles DNS-Adressen werden auf meine beiden Server umgeschrieben. Die Geräte glauben, Google habe geantwortet – tatsächlich war es Technitium, inklusive Filterlisten.</p>
<h3 id="regel-3-und-was-ist-mit-dns-over-https">Regel 3: Und was ist mit DNS-over-HTTPS?</h3>
<p>DoH ist der Endgegner dieser Konstruktion: DNS-Anfragen, verpackt in normales HTTPS über Port 443. Für die Firewall sieht das aus wie jeder andere Webseitenaufruf, eine Portsperre greift nicht.</p>
<p>Hier kommen die zweite Filterliste und die Firefox-Kanarienvogel-Domain ins Spiel: Technitium löst die Namen bekannter DoH-Anbieter einfach nicht auf. Ein Gerät, das per DoH an mir vorbei will, findet seinen DoH-Server gar nicht erst – solange es ihn über Namen sucht. Eine Lücke bleibt: Wer seinen DoH-Server fest per IP kennt, kommt durch. Das nehme ich bewusst in Kauf.</p>
<h3 id="bonus-ein-zweiter-filter-auf-der-firewall">Bonus: Ein zweiter Filter auf der Firewall</h3>
<p>Die Regel, mit der die Geräte die DNS-Server erreichen dürfen, hat zusätzlich das <strong>DNS-Filterprofil</strong> der FortiGate. Die FortiGate schaut sich also jede Anfrage auf dem Weg zu Technitium an und blockt Kategorien wie Schadsoftware, Phishing und frisch registrierte Domains anhand der Herstellerdaten. Zwei unabhängige Filter, die sich ergänzen.</p>
<h2 id="die-bewusste-ausnahme-die-firewall-selbst">Die bewusste Ausnahme: die Firewall selbst</h2>
<p>Die FortiGate fragt <strong>nicht</strong> meine Technitium-Server, sondern den DNS-Dienst ihres Herstellers, ebenfalls verschlüsselt per DoT. Das ist Absicht.</p>
<p>Die Firewall braucht DNS für ihre eigenen Updates, für Filterdaten, für Regeln mit Domainnamen und für die Prüfung, ob die Internetleitung läuft. Würde sie Technitium fragen, hinge sie an Proxmox. Startet der Proxmox-Host neu oder hat er ein Problem, stünde auch die Firewall ohne DNS da – genau in dem Moment, in dem ich sie am dringendsten brauche. Also bleibt sie unabhängig.</p>
<h2 id="die-ptr-flut-von-home-assistant">Die PTR-Flut von Home Assistant</h2>
<p>Kurz nach dem Umstieg füllte sich mein Firewall-Log mit Hunderten geblockten Anfragen, immer zur gleichen Minute jeder Stunde. Absender: Home Assistant.</p>
<p>Dessen Geräteerkennung fragt regelmäßig für das <strong>komplette Netz</strong> nach, welcher Name zu welcher IP gehört. Diese Rückwärtsabfragen heißen <strong>PTR-Abfragen</strong>. Gerichtet waren sie an das Gateway, also an die FortiGate – und die hatte keinen DNS-Dienst, also hat sie jede einzelne verworfen. Rund 700 pro Stunde, alle im Log.</p>
<p>Die Lösung: Auf der Schnittstelle zum Server-Netz, und nur dort, beantwortet die FortiGate DNS-Anfragen jetzt selbst, indem sie sie weiterreicht:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">config system dns-server
</span></span><span class="line"><span class="cl">    edit &#34;&lt;SERVER-INTERFACE&gt;&#34;
</span></span><span class="line"><span class="cl">        set mode forward-only
</span></span><span class="line"><span class="cl">    next
</span></span><span class="line"><span class="cl">end
</span></span></code></pre></div><p>Die PTR-Anfragen für private Adressen bekommen damit ein schnelles „gibt es nicht“ (<code>NXDOMAIN</code>), und das Log ist wieder ruhig. Getestet habe ich das mit:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">dig -x 10.0.10.20 @10.0.10.254
</span></span></code></pre></div><h2 id="stolperfallen--learnings">Stolperfallen &amp; Learnings</h2>
<p><strong>Beim Update braucht Technitium ein paar Sekunden.</strong> Ich habe beide Server mit dem offiziellen Installationsskript aktualisiert und direkt danach getestet: <code>connection refused</code>. Kurzer Schreck – dabei braucht Technitium nach dem Neustart einfach ein paar Sekunden, bis er wieder auf Port 53 lauscht. Seitdem gilt: nach dem Update kurz warten, und <strong>erst den Secondary aktualisieren, testen, dann den Primary</strong>. So beantwortet immer mindestens ein Server die Anfragen.</p>
<p><strong>„Server-Reset“ heißt nicht „blockiert“.</strong> Im Firewall-Log sah ich bei den DoT-Verbindungen zu Quad9 lauter Einträge mit <code>server-rst</code> und dachte zuerst, die Firewall würde DoT blocken. Tatsächlich ist das nur der Grund, warum eine Verbindung beendet wurde: Quad9 hat sie nach getaner Arbeit zugemacht. Ein echter Block steht dort als <code>deny</code>. In den Einträgen standen außerdem übertragene Bytes in beide Richtungen – es lief also alles.</p>
<p><strong>Manche Geräte sind DNS-Querulanten.</strong> Ohne die Umleitung für Googles DNS wären meine Echos nach Regel 1 einfach verstummt. Wer DNS sperrt, sollte vorher ins Log schauen, wer bisher wohin gefragt hat.</p>
<p><strong>Ich habe ein Feature übersehen.</strong> Gebaut habe ich die Redundanz klassisch: Primary, Secondary, Zonentransfer. So habe ich das in meiner Laufbahn irgendwie immer gemacht, und auf den Cluster bin ich schlicht nicht gekommen. Erst beim Schreiben dieses Artikels ist mir aufgefallen, dass Technitium seit Version 14 einen eigenen <strong>Cluster</strong> kann – also schon in der Version, die ich installiert habe.</p>
<p>Der Unterschied ist nicht klein:</p>
<ul>
<li><strong>Zonentransfer</strong> gleicht nur die Zonen ab. Alles andere – Filterlisten, Einstellungen, Benutzer – pflege ich auf beiden Servern doppelt. Dafür ist es Standard-DNS und funktioniert mit jeder DNS-Software.</li>
<li><strong>Ein Cluster</strong> verwaltet die Konfiguration zentral: Filterlisten, Einstellungen, Apps und Benutzer kommen vom primären Knoten, Zonen lassen sich über eine Katalog-Zone mitnehmen. Voraussetzung ist unter anderem HTTPS für die Weboberfläche, und den Cluster-Domainnamen kann man später nicht mehr ändern.</li>
</ul>
<p>Für mein Setup wäre der Cluster vermutlich die bequemere Wahl gewesen – ausgerechnet der Abgleich zwischen zwei Servern war ja einer meiner Gründe für Technitium. Kaputt ist nichts – aber ich pflege seit Wochen Dinge doppelt, die ich nicht doppelt pflegen müsste. Ist blöd, lässt sich aber korrigieren. 😉</p>
<h2 id="fazit">Fazit</h2>
<p>DNS im Heimnetz selbst zu betreiben, ist kein Hexenwerk, aber erst mit der Firewall wird es rund: Die Technitium-Server liefern Namen, Filter und Verschlüsselung, die FortiGate sorgt dafür, dass niemand an ihnen vorbeikommt. Dazu eine bewusste Ausnahme für die Firewall selbst, damit sie nicht von Proxmox abhängt. Und eine Lektion zum Schluss: Was man seit Jahren so macht, ist nicht automatisch der beste Weg. Nachlesen, was die Software inzwischen kann, lohnt sich.</p>
<h2 id="was-als-nächstes-kommt">Was als Nächstes kommt</h2>
<ul>
<li><strong>Umbau auf den Technitium-Cluster</strong> – mit eigenem Artikel, wenn es so weit ist.</li>
<li><strong>AirPrint über VLAN-Grenzen per DNS</strong>: Mein Drucker steht in einem eigenen Netz, und die iPhones finden ihn trotzdem. Wie das mit ein paar DNS-Einträgen klappt, kommt in einem eigenen Artikel.</li>
</ul>
]]></content:encoded></item></channel></rss>