IT security, FreeBSD, Linux, mail server hardening, post-quantum crypto, DNS, retro computing & hands-on hardware hacks. Privater Tech-Blog seit 2003.

Kategorie: Kernel-Error-Blog (Seite 1 von 51)

Persönlicher Tech-Blog von Sebastian van de Meer — Beiträge zu IT-Security, Netzwerken, FreeBSD, Linux, Elektronik und Maker-Projekten.

Postfix ohne Klartext-Fallback: DANE, sonst MTA-STS, sonst ein geprüftes Zertifikat

Schematische Darstellung eines Postfix-Mailservers mit TLS-Absicherung über DANE, MTA-STS und Zertifikatsprüfung. Ein unsicherer Klartext-Fallback durch STARTTLS-Downgrade wird blockiert.

Mein Mailserver hat seit Jahren DANE mit DNSSEC, MTA-STS und seit diesem Jahr auch einen Post-Quantum-Schlüsselaustausch. Trotzdem hat er heute eine Testmail im Klartext ausgeliefert. Das war kein Bug, sondern genau das Verhalten, das in der Postfix-Doku steht. In diesem Beitrag geht es darum, warum Postfix die Policy „DANE wenn vorhanden, sonst ein geprüftes Zertifikat“ nicht von sich aus kann, warum ich dafür unter FreeBSD nichts Fertiges gefunden habe und wie mein kleiner Dienst postfix-dane-policy diese Lücke schließt.

Der Auslöser: ein Downgrade-Test

Der Mail-Server-Test von comcrypto simuliert unter anderem einen STARTTLS-Downgrade, den eigenen Server kannst du dort also selbst prüfen. Man schickt eine Mail an eine Testadresse, und der Empfängerserver lässt den TLS-Handshake absichtlich scheitern. So würde es auch ein aktiver Angreifer zwischen zwei Mailservern machen: STARTTLS stören und schauen, ob der Absender danach ohne Verschlüsselung weitermacht. Mein Postfix hat genau das getan:

postfix/smtp[5497]: 2BD2E39781: to=<xxxx@evaluation.comcrypto.de>, relay=smtp-in-nbg1pr01loearatl.mxg.cloud[37.120.186.126]:25, delay=1.2, tls=may?, dsn=4.7.5, status=deferred (Cannot start TLS: handshake failure)
postfix/smtp[6005]: 2BD2E39781: to=<xxxx@evaluation.comcrypto.de>, relay=smtp-in-nbg1pr01loearatl.mxg.cloud[37.120.186.126]:25, delay=490, tls=may:none, dsn=2.0.0, status=sent (250 2.0.0 Ok: queued as C7DC6E00C5)

Erster Versuch: Handshake scheitert, Mail wird zurückgestellt. Gut acht Minuten später der zweite Versuch, und da steht tls=may:none mit status=sent. Die Mail ist im Klartext beim Empfänger angekommen. Ein Angreifer, der STARTTLS stört, hätte sie also nur ein paar Minuten lang aufhalten müssen.

Das ärgert mich vor allem, weil ich dachte das Thema wäre bei mir erledigt. Ist es aber nur für Empfänger, die selbst DANE oder MTA-STS veröffentlichen. Für alle anderen gilt opportunistisches TLS nach RFC 7435: verschlüsseln, wenn es geht, und wenn nicht, dann eben nicht. Und das ist immer noch ein großer Teil der Mailserver da draußen.

Warum Postfix das allein nicht kann

Die ausgehende TLS-Stufe stellt man in Postfix mit smtp_tls_security_level ein. Die Manpage postconf(5) von Postfix 3.11.7 ist bei den einzelnen Stufen sehr deutlich. Zu may steht dort:

after a failed TLS handshake or TLS session, fall back to plaintext if the message has spent minimal_backoff_time in the mail queue

Genau das zeigt mein Log oben. Bei mir stand allerdings gar nicht may in der main.cf, sondern dane. Dazu sagt die Manpage:

When DNSSEC-validated TLSA records are not found the effective tls security level is „may“.

Mit dane bekommt also jeder Empfänger ohne TLSA-Record wieder may, inklusive Klartext-Fallback. Die naheliegende Idee wäre jetzt, global auf verify oder secure zu gehen. Beide Stufen ignorieren TLSA-Records aber komplett. Das hätte zwei unschöne Folgen:

  • Empfänger mit DANE-EE und einem selbst signierten Zertifikat wären plötzlich nicht mehr erreichbar. Das ist bei kleinen Selbsthostern gar nicht so selten und mit DANE völlig legitim, das Vertrauen kommt dort ja aus dem DNSSEC-gesicherten TLSA-Record und nicht von einer CA.
  • DANE-Empfänger würden nur noch gegen die CA geprüft und nicht mehr gegen ihr TLSA. Damit verschenkt man genau den Schutz, den DANE bietet.

Was ich eigentlich will, ist eine Stufe „DANE, wenn der Empfänger es veröffentlicht, sonst verify“. Die gibt es nicht. Viktor Dukhovni, der DANE in Postfix gebaut hat, schrieb dazu schon 2021 auf der exim-users-Liste: „It isn’t currently possible to do ‚dane‘ else ‚encrypt‘. Need a dane-fallback level“. Auch Postfix 3.11 und die 3.12-Snapshots bis August 2026 haben so etwas nicht.

Gibt es das schon fertig?

Postfix kann die Entscheidung aber pro Ziel treffen lassen. Über smtp_tls_policy_maps fragt Postfix vor jeder Zustellung eine Tabelle, welche TLS-Stufe für die Zieldomain gelten soll. Diese Tabelle darf auch ein Dienst sein, der live im DNS nachschaut. Bevor ich selbst etwas baue, habe ich also gesucht:

  • postfix-mta-sts-resolver von Snawoot kann nur MTA-STS, DANE ist dort bewusst außen vor (Issue #67). Den nutze ich seit 2019, damals habe ich das Logfile und ein rc.d-Skript für FreeBSD beigesteuert. Inzwischen gibt es ihn als FreeBSD-Port und er bleibt bei mir im Einsatz, jetzt als zweite Map.
  • postfix-tlspol von Zuplu kommt am nächsten: DANE und MTA-STS in einem Go-Daemon. Ein gutes Projekt, nur eben nicht für FreeBSD gedacht. Es gibt keinen Port, Release-Binaries habe ich nur für Linux gefunden, der Build braucht ein sehr aktuelles Go, und die Installationswege sind auf systemd oder Docker ausgelegt. Dazu liefert tlspol bei gemischten MX-Sets nach meinem Stand dane statt dane-only, womit dort wieder may möglich ist.
  • Mailu nutzt intern eine ähnliche Kette, setzt global aber dane, ohne DANE oder MTA-STS landet man also wieder bei may. Und es ist Docker. Bei mailcow, docker-mailserver, Mail-in-a-Box, iRedMail und Modoboa habe ich nichts Vergleichbares gefunden.
  • Im FreeBSD-Paket-Repository steckt DANE natürlich in Postfix selbst, dazu gibt es Werkzeuge wie letsdns. Das verwaltet aber die eigenen TLSA-Records, kümmert sich also um die andere Seite.

Für Postfix unter FreeBSD habe ich Stand jetzt also nichts Fertiges gefunden. Dann eben selbst bauen.

postfix-dane-policy

postfix-dane-policy ist ein kleiner Python-Dienst (Python 3.10 oder neuer, dazu dnspython). Er spricht das socketmap-Protokoll von Postfix, das sind simple Netstrings über einen Unix-Socket, und wird in smtp_tls_policy_maps eingehängt. Er beantwortet genau eine Frage: Hat diese Zieldomain nutzbares DANE? Alles andere bleibt bei Postfix und beim MTA-STS-Resolver.

Für jede Next-Hop-Domain holt der Dienst den MX-RRset und für jeden MX-Host den TLSA-RRset unter _25._tcp. Ist ein MX-Host ein per DNSSEC validierter CNAME, macht er es wie Postfix und RFC 7672: Er sucht zuerst am Zielnamen, dann am ursprünglichen Namen, der erste gesicherte TLSA-RRset entscheidet, und ein Fehler beim Lookup bricht die Suche ab. Gezählt werden nur brauchbare Records, also Usage 2 oder 3, Selector 0 oder 1 und Matching-Type 0, 1 oder 2 mit passender Länge. Dann entscheidet er so:

SituationAntwortWirkung in Postfix
MX per DNSSEC validiert und mindestens ein MX-Host hat einen validierten TLSA-RRset mit brauchbaren RecordsOK dane-onlyDANE ist Pflicht, kein Fallback
MX nicht validiert, kein TLSA, nur unbrauchbare TLSA-Records, Null-MX, Domain existiert nichtNOTFOUNDnächste Map (MTA-STS) oder die globale Stufe
SERVFAIL, bogus DNSSEC oder Timeout beim MX-Lookup, oder beim TLSA-Lookup, wenn kein anderer MX brauchbares TLSA hatTEMPZustellung wird zurückgestellt, nie herabgestuft
Elterndomain (.example.com) oder [host]:port als Next-HopNOTFOUND

Die eigentliche Logik steckt in einer einzigen Funktion. Gekürzt sieht decide() aus policy.py so aus:

def decide(domain, backend, deadline=None):
    """Return (result, ttl, reason) for a normalized next-hop domain."""
    mx = query(domain, "MX")
    if mx.status == "fail":
        return RESULT_TEMP, 0, "MX lookup failed"
    if mx.status == "nxdomain":
        return RESULT_NONE, NEGATIVE_TTL, "domain does not exist"
    if not mx.secure:
        return RESULT_NONE, NEGATIVE_TTL, "MX not DNSSEC-validated"

    # ... null MX, implicit MX (no MX record = domain itself) ...

    tlsa_hosts = []
    failed_hosts = []
    for host in hosts:
        # follows a secure CNAME, counts only usable TLSA records
        found, failed, ttl = _tlsa_for_host(host, query)
        if found:
            tlsa_hosts.append(host)
            ttls.append(ttl)
        elif failed:
            failed_hosts.append(host)

    if tlsa_hosts:
        ttl = max(POSITIVE_TTL_MIN, min([POSITIVE_TTL_MAX] + ttls))
        return RESULT_DANE, ttl, "secure TLSA on " + ",".join(tlsa_hosts)
    if failed_hosts:
        return RESULT_TEMP, 0, "TLSA lookup failed for " + ",".join(failed_hosts)
    return RESULT_NONE, NEGATIVE_TTL, "no secure TLSA"

Ein paar Entscheidungen dahinter sind mir wichtig:

  • Der Dienst validiert DNSSEC nicht selbst. Er vertraut dem AD-Bit eines lokalen, validierenden Resolvers. Unter FreeBSD ist das typischerweise local_unbound aus dem Basissystem, das lauscht schon auf 127.0.0.1 und passt damit zu den Standardwerten des Dienstes.
  • Gemischte MX-Sets bekommen trotzdem dane-only. Haben nur manche MX-Hosts einen TLSA-Record, stellt Postfix dann nur an die Hosts zu, die es per DANE authentifizieren kann, und überspringt die anderen. Bei dane würde es auf ungeprüftes TLS zurückfallen. Das ist der bewusste Unterschied zu tlspol.
  • Fehler führen zu „später nochmal“. Ein SERVFAIL oder bogus DNSSEC ist genau das, was ein Angreifer am DNS provozieren würde. Darauf antwortet der Dienst mit TEMP, und Postfix stellt die Mail zurück. Fällt nur der TLSA-Lookup bei einem MX aus, während ein anderer MX brauchbares TLSA hat, antwortet er dane-only. Postfix prüft dann jeden Host selbst. Ist der Dienst weg, kann Postfix die Map nicht abfragen und stellt ebenfalls zurück. Setzt der Resolver beim regelmäßigen Selbsttest gegen die signierte Root-Zone kein AD-Bit, antwortet der Dienst grundsätzlich mit TEMP. In keinem Fall wird herabgestuft.
  • Cache im Speicher. Positive Antworten leben so lange wie die TTL der Records, mindestens 60 Sekunden und höchstens eine Stunde. Negative Antworten werden 5 Minuten gehalten, TEMP nie. Jede Entscheidung landet per syslog im maillog.
  • Nur Postfix darf fragen. Der Dienst läuft als eigener User danepolicy, den der Port anlegt. Der Socket liegt in einem Verzeichnis, das danepolicy:postfix gehört und Modus 0750 hat. Andere lokale Prozesse kommen nicht dran, und es gibt Limits für gleichzeitige Verbindungen und die Dauer der Lookups.

So sieht das im maillog aus:

postfix-dane-policy[13399]: domain=posteo.de result=dane reason=secure TLSA on mx01.posteo.de,mx02.posteo.de,mx03.posteo.de,mx04.posteo.de
postfix-dane-policy[13399]: domain=gmail.com result=none reason=MX not DNSSEC-validated
postfix-dane-policy[13399]: domain=evaluation.comcrypto.de result=none reason=no secure TLSA

Das Zusammenspiel in der main.cf

Der eigentliche Trick ist die Kombination aus Policy-Maps und globaler Stufe:

smtp_tls_security_level = verify
smtp_dns_support_level = dnssec
smtp_tls_policy_maps =
    socketmap:unix:/var/run/postfix_dane_policy/policy.sock:dane,
    socketmap:inet:127.0.0.1:8461:mta-sts
smtp_tls_CAfile = /usr/local/share/certs/ca-root-nss.crt
smtp_tls_mandatory_protocols = >=TLSv1.2
tls_required_enable = no
delay_warning_time = 4h

Postfix fragt die Maps der Reihe nach, der erste Treffer gewinnt. Zuerst kommt postfix-dane-policy. Hat der Empfänger DANE, gilt dane-only. Sonst fragt Postfix den MTA-STS-Resolver, und veröffentlicht der Empfänger eine MTA-STS-Policy im Modus enforce, gilt secure gegen die Hostnamen aus der Policy. Liefert keine der beiden Maps etwas, greift die globale Stufe verify: Das Zertifikat muss von einer vertrauenswürdigen CA stammen und zum MX-Hostnamen passen. DANE vor MTA-STS ist dabei keine Geschmacksfrage, so will es auch RFC 8461.

Das Ergebnis ist die Kette, die ich haben wollte: DANE, wenn veröffentlicht, sonst MTA-STS, sonst ein CA-geprüftes Zertifikat und niemals Klartext. smtp_dns_support_level = dnssec braucht Postfix, damit es selbst die AD-Bits auswertet und dane-only überhaupt umsetzen kann. Mit delay_warning_time erfährt ein Absender nach vier Stunden, dass seine Mail hängt, statt erst Tage später einen Bounce zu bekommen.

Drei Zeilen sehen unscheinbar aus, sind aber wichtig. smtp_tls_CAfile braucht verify und MTA-STS, sonst gibt es nichts, wogegen Postfix ein Zertifikat prüfen könnte. Unter FreeBSD kommt das Bundle aus dem Paket ca_root_nss. Die TLS-Mindestversion steht bewusst in smtp_tls_mandatory_protocols und nicht in smtp_tls_protocols. Letzteres gilt nur für opportunistisches TLS, bei verify, secure und dane-only zieht die mandatory-Variante, und deren Default erlaubt noch TLS 1.0. Die Manpage rät bei DANE übrigens davon ab, TLS 1.0 abzuschalten, weil ein paar DANE-Server nichts anderes können. Ich nehme das in Kauf.

Und tls_required_enable = no hat mich selbst überrascht. Seit Postfix 3.10 darf ein Absender per Header TLS-Required: no (RFC 8689) darum bitten, die TLS-Policy für seine Mail zu ignorieren. Postfix fragt dann keine Policy-Maps mehr und stellt mit may zu, also notfalls im Klartext. Das ist standardmäßig eingeschaltet. Ohne diese Zeile wäre „niemals Klartext“ also gelogen.

Vorher und nachher

Zum Prüfen eignet sich sendmail -bv sehr gut. Damit macht der Postfix-Client einen echten SMTP-Durchlauf zum Empfänger, inklusive TLS-Policy, stellt aber nichts zu. Das Ergebnis kommt als Bericht an den Absender und steht im maillog. Das sind die Werte von meinem Server vor und nach der Umstellung:

Zielvorherjetzt
posteo.dedane, Verifiedtls=dane-only, Verified
mailbox.orgdanetls=dane-only, Verified, sogar per Raw Public Key (RFC 7250)
gmail.comsecure (MTA-STS)tls=secure, Verified, Schlüsselaustausch X25519MLKEM768
Microsoft 365 (Kundendomain)may, nur „Trusted“tls=verify, Verified
verifier.port25.commay, „Untrusted“, trotzdem zugestellttls=!verify, 4.7.5, Zertifikat abgelaufen
comcrypto Downgrade-TestKlartext zugestellt (tls=may:none)Handshake-Fehler, 4.7.5, kein Klartext-Fallback

Die mailbox.org-Zeile finde ich schön, weil sie zeigt, was DANE kann und eine CA-Prüfung nicht. mailbox.org legt dort gar kein klassisches Zertifikat vor, sondern nur den nackten öffentlichen Schlüssel. Geprüft wird er gegen den TLSA-Record:

postfix/smtp[11293]: Verified TLS connection established to mx2.mailbox.org[2001:67c:2050:104:0:2:25:1]:25: TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519MLKEM768 server-signature RSA-PSS (4096 bit raw public key) server-digest SHA512
postfix/smtp[11293]: 1A23038B9A: to=<postmaster@mailbox.org>, relay=mx2.mailbox.org[2001:67c:2050:104:0:2:25:1]:25, delay=0.6, tls=dane-only, dsn=2.1.5, status=deliverable (250 2.1.5 Ok)

Die port25-Zeile ist meine Lieblingspointe. verifier.port25.com ist ein bekannter Dienst, um SPF, DKIM und DMARC zu testen, und ich schicke da seit Jahren Testmails hin. Erst heute ist aufgefallen, dass deren Zertifikat abgelaufen ist. Vorher war das meinem Postfix schlicht egal:

postfix/smtp[11301]: 2336538BA5: to=<check-auth@verifier.port25.com>, relay=verifier.port25.com[2600:1f14:a73:9b01:d769:5be1:de28:5251]:25, delay=1.8, tls=!verify, dsn=4.7.5, status=undeliverable (Server certificate not verified)

Und dann natürlich der eigentliche Gegenbeweis. Nach der Umstellung habe ich den comcrypto-Test mit einer neuen Testmail wiederholt. Statt im Klartext zugestellt zu werden, liegt sie jetzt in der Queue:

-Queue ID-  --Size-- ----Arrival Time---- -Sender/Recipient-------
DA4C538AD7     9844 Fri Nov 13 15:04:25  xxxx@kernel-error.com
                                         (Cannot start TLS: handshake failure)
                                         xxxx@evaluation.comcrypto.de

-- 9 Kbytes in 1 Request.

Im maillog steht bei jedem neuen Versuch tls=verify? und status=deferred, auch beim Wechsel auf die IPv6-Adresse des Empfängers. Nach vier Stunden bekommt der Absender eine Verzögerungswarnung, nach maximal_queue_lifetime einen Bounce. Das ist genau das Verhalten, das ich haben will: Lieber kommt eine Mail gar nicht an als unbemerkt im Klartext.

Probes sind das eine, echte Zustellungen das andere. Kurz nach der Umstellung habe ich deshalb noch Testmails an ein paar öffentliche Prüfdienste geschickt. Das maillog sagt dazu:

ZielAntwort des DienstesPostfixErgebnis
mecsa.dcslab.eu (MECSA, Mail-Security-Checker der EU-Kommission)danetls=dane-onlyzugestellt, per DANE verifiziert
mail.binsec.servicesnonetls=verifyzugestellt, Verified
mailsrv1.captaindns.comnonetls=verifyzugestellt, Verified
verifier.port25.comnonetls=!verifyzurückgehalten, Zertifikat abgelaufen
ein Wegwerf-Mail-Dienstnoneerst „TLS is required, but was not offered by host“, später tls=!verifyzurückgehalten, kein bzw. kein gültiges TLS

Über den ganzen Testzeitraum stehen im Log null Klartext-Zustellungen, kein fatal oder panic, kein einziger Socketmap-Fehler auf Postfix-Seite, und der Dienst lief durchgehend mit derselben PID. Die Prüfdienste, die selbst sauberes TLS oder DANE machen, bekommen ihre Mail ohne Probleme. Zurückgehalten wird nur, was die Anforderungen wirklich nicht erfüllt.

Nachbauen auf FreeBSD

Die Referenz ist das README im Repository, aktuell ist Version 0.3.1. Der Port liegt im Repository, die Einreichung in den offiziellen Ports-Tree folgt noch. Ein pkg install gibt es also noch nicht.

Zuerst braucht es einen validierenden Resolver. Der aus dem Basissystem reicht völlig:

sysrc local_unbound_enable=YES
service local_unbound start
drill -D posteo.de MX @127.0.0.1 | grep flags   # muss "ad" zeigen

Dann den Port aus dem Repository bauen, solange er nicht im Ports-Tree ist, und den Dienst starten. Der Port legt dabei den User danepolicy an und bringt dessen Definition selbst mit, solange der User im Ports-Tree noch nicht registriert ist:

git clone https://github.com/Kernel-Error/postfix-dane-policy
cp -R postfix-dane-policy/freebsd/mail/py-postfix-dane-policy /usr/ports/mail/
cd /usr/ports/mail/py-postfix-dane-policy && make install clean

sysrc postfix_dane_policy_enable=YES
service postfix_dane_policy start

Standardmäßig lauscht der Dienst auf dem Unix-Socket /var/run/postfix_dane_policy/policy.sock und fragt den Resolver auf 127.0.0.1, das passt zu local_unbound. Läuft Unbound woanders, etwa in einer Jail, setzt du die Adresse mit --dns über postfix_dane_policy_args in der rc.conf. TCP geht auch noch, ist aber nicht empfohlen, weil sich während eines Neustarts ein anderer lokaler Prozess den Port schnappen könnte. Ein kurzer Test von Hand:

postmap -q posteo.de socketmap:unix:/var/run/postfix_dane_policy/policy.sock:dane
# dane-only
postmap -q gmail.com socketmap:unix:/var/run/postfix_dane_policy/policy.sock:dane
# (leer, also NOTFOUND)

Für MTA-STS als zweite Map gibt es den Port mail/py-postfix-mta-sts-resolver, der lauscht standardmäßig auf Port 8461. Fehlt noch das CA-Bundle, kommt es mit pkg install ca_root_nss. Danach die main.cf wie oben anpassen, postfix reload, und mit sendmail -bv postmaster@posteo.de prüfen. Im maillog sollte dann je nach Ziel tls=dane-only, tls=secure oder tls=verify auftauchen.

Was du vorher wissen musst

Global verify hat Folgen, und die sollte man vorher kennen:

  • Empfänger ohne STARTTLS oder mit ungültigem Zertifikat sind nicht mehr erreichbar. Selbst signiert ohne DANE, abgelaufen oder falscher Name: Die Mail bleibt in der Queue, der Absender bekommt eine Warnung und später einen Bounce. Einen ersten Überblick gibt dein maillog: grep postfix/smtp /var/log/maillog | grep -E "(Untrusted|Anonymous) TLS connection|tls=[a-z]*:none". Das ist aber nur eine Vorauswahl. Auch eine Verbindung, die bisher als „Trusted“ geloggt wurde, kann an der Namensprüfung von verify scheitern. Den echten Test machst du mit sendmail -bv an die Ziele, die dir wichtig sind.
  • Bei mir hat es eine Weiterleitung erwischt. Meine Spamtraps haben ihre Mails bisher an einen externen Server ohne STARTTLS weitergereicht, rund 180 Mails pro Woche im Klartext. Die Weiterleitung habe ich abgeschaltet. Die Traps nehmen weiter an, rspamd lernt daraus, und danach wird lokal per discard: verworfen.
  • Wegwerf-Adressen und kleine Testdienste merken es zuerst. Solche Dienste haben oft gar kein oder kein gültiges TLS. Die Mail bleibt dann in der Queue, mailq zeigt dir den Grund, und nach delay_warning_time kommt die Warnung an den Absender.
  • verify hat eine eingebaute Grenze. Geprüft wird gegen den MX-Hostnamen. Ist der MX-Lookup des Empfängers nicht per DNSSEC gesichert, kann ein aktiver Angreifer die MX-Antwort fälschen und die Mail auf seinen eigenen Server umleiten, der ein gültiges Zertifikat für seinen eigenen Namen vorlegt. Diese Lücke schließen DANE und MTA-STS, oder fest eingetragene Identitäten pro Ziel. verify ist trotzdem deutlich besser als Klartext: STARTTLS zu stören reicht nicht mehr, der Angreifer muss DNS fälschen und einen eigenen Server mit passendem Zertifikat betreiben.
  • Der Dienst glaubt dem AD-Bit. Betreib ihn deshalb nur mit einem lokalen, validierenden Resolver und nicht mit einem entfernten über ein Netz, dem du nicht traust. Der Selbsttest fängt einen Resolver ab, der gar nicht validiert, aber keinen, der lügt.

Mein Fazit

Vor über zehn Jahren habe ich hier im Blog noch beschrieben, wie man Postfix ausgehend mit smtp_tls_security_level=may zum Verschlüsseln bringt. Damals war das ein Fortschritt. Heute ist es die letzte Downgrade-Lücke für ausgehende Mail, und die ist jetzt bei mir zu. DANE-Empfänger mit selbst signierten Zertifikaten bleiben erreichbar, DNS-Probleme oder ein ausgefallener Dienst führen zu „später nochmal“ und nie zu Klartext. Der Dienst ist klein, die Logik steckt in einer Datei, er ist MIT-lizenziert und lässt sich mit dem vorhandenen MTA-STS-Resolver kombinieren.

Wenn du Postfix unter FreeBSD betreibst, probier es gern aus. Issues und Pull-Requests auf GitHub sind willkommen, gerade Rückmeldungen von anderen Setups helfen mir, bevor der Port in den offiziellen Tree geht. Und falls ich irgendwo falsch liege, korrigiert mich bitte.

Quellen: RFC 7672 (SMTP Security via Opportunistic DANE TLS), RFC 8461 (MTA-STS), RFC 7435 (Opportunistic Security), Postfix TLS_README und postconf(5) zu smtp_tls_security_level.

Siehe auch: Postfix mit DANE und DNSSEC absichern, MTA-STS einrichten, Postfix: Eingehende E-Mails ohne TLS ablehnen, Post-Quantum TLS für E-Mail und Postfix 3.11.1 mit OpenSSL 3.5.

Fragen? Dann einfach fragen.

BIOS erklärt, Teil 13: PXE, iSCSI und eine Netzwerkkarte die sich über sich selbst beschwert

Diese Maschine hat vier Netzwerkanschlüsse mit jeweils zehn Gigabit. Zwei davon sitzen fest auf dem Board, zwei kommen von einer Steckkarte. Genutzt wird einer. Die anderen drei sind absichtlich abgeschaltet, weil ich sie nicht brauche.

Im BIOS ist davon nichts zu merken: dort bekommt jede der vier Schnittstellen ihr eigenes Menü, mit Untermenüs, und in Summe sind das rund fünfzig Bildschirmseiten. Für einen Rechner unter dem Schreibtisch ist das absurd. Für ein Board, das eigentlich in einem Rack ohne Tastatur steht, ergibt es Sinn, und dieser Unterschied ist im Grunde das Thema der Folge.

Teil 13, gesammelt unter BIOS & Firmware.

Der Hauptschalter

Aptio Setup, Network Configuration, mit Network Stack, PXE für IPv4 und IPv6 und den vier Netzwerkschnittstellen
Network Stack                     [Enabled]
IPv4 PXE Support                  [Enabled]
IPv4 HTTP Support                 [Disabled]
IPv6 PXE Support                  [Enabled]
IPv6 HTTP Support                 [Disabled]
PXE Boot Wait Time                0
Media Detect Count                1

Network Stack ist die Grundsatzentscheidung: bringt die Firmware überhaupt einen eigenen Netzwerkstapel mit? Aus heisst, dass es kein PXE gibt, kein iSCSI, kein HTTP-Boot, und dass die entsprechenden Menüs verschwinden.

PXE ist die klassische Variante des Netzwerkstarts: Rechner fragt per DHCP nach einer Adresse und bekommt gleich noch gesagt, von welchem Server er sein Startabbild holen soll. Übertragen wird per TFTP, einem Protokoll ohne jede Absicherung, das seit den Achtzigern im Wesentlichen unverändert ist. In einem abgeschotteten Verwaltungsnetz ist das in Ordnung, im normalen LAN sollte man wissen was man tut.

HTTP Support ist die moderne Antwort darauf: dasselbe über HTTP, optional mit TLS und Zertifikatsprüfung. Deshalb gibt es auf diesem Board auch eine eigene Seite für TLS-Zertifikate. Bei mir ist HTTP-Boot aus, weil ich weder das eine noch das andere nutze.

Media Detect Count 1 legt fest, wie oft die Firmware nachsieht, ob ein Kabel steckt, bevor sie aufgibt. Bei drei nicht angeschlossenen Ports ist die Eins eine sinnvolle Sparmassnahme. PXE Boot Wait Time 0 bedeutet, dass sie nicht extra wartet.

Vier Karten, vier Menüs

Aptio Setup, Menü einer Netzwerkkarte, mit Firmware Image Properties, NIC Configuration und Blink LEDs

Jede Schnittstelle bekommt einen eigenen Eintrag mit demselben Aufbau. Für die Onboard-Ports sieht das so aus:

> Firmware Image Properties
    Option ROM version            1.3082.0
    NVM Version                   3.5
> NIC Configuration
Blink LEDs                        0
UEFI Driver                       Intel(R) 10GbE Driver 8.1.00 x64
Device Name                       Intel(R) Ethernet Controller X550
Chip Type                         Intel X550
PCI Device ID                     1563
Link Status                       [Disconnected]

Blink LEDs ist die praktischste Funktion des ganzen Menüs und die einzige, die man im Rechenzentrum wirklich vermisst, wenn sie fehlt: man gibt eine Anzahl Sekunden ein und die Leuchtdiode am betreffenden Anschluss blinkt. So findet man heraus, welches der zwölf Kabel hinten am Gerät zu dieser Schnittstelle gehört, ohne raten zu müssen.

Unter NIC Configuration stehen dann Dinge wie Link Speed, Wake on LAN und Legacy Boot Protocol. Der letzte Punkt entscheidet, ob die Karte im Vorstart PXE spricht, iSCSI, oder gar nichts.

iSCSI im BIOS, und wofür das gut ist

Bei der Steckkarte kommt noch ein Untermenü dazu, das ich beim Durchklicken zuerst für einen Irrtum gehalten habe:

> iSCSI Configuration
    iSCSI Initiator Parameters
    CHAP Authentication
    iSCSI First Target

Eine vollständige iSCSI-Konfiguration im BIOS, inklusive Anmeldeverfahren. Die Idee dahinter ist ein Rechner ohne eigene Festplatte: die Systemplatte liegt auf einem Speichersystem im Netz, die Firmware baut die Verbindung auf, bevor irgendein Betriebssystem existiert, und von aussen sieht es aus wie eine ganz normale lokale Platte.

Für Serverfarmen ist das ein sinnvolles Modell. Hardware defekt heisst dann: Blech tauschen, Konfiguration übernehmen, weiter. Für meine Workstation ist es vollkommen sinnlos, und ich erwähne es nur, weil es einem beim Durchsehen des Menüs unweigerlich begegnet und man sich fragt, was das hier zu suchen hat.

CHAP Authentication daneben ist übrigens ein gutes Beispiel für die Alterung solcher Protokolle: das ist ein Challenge-Response-Verfahren, das für sich genommen kein böses Design ist, aber ohne Transportverschlüsselung eingesetzt wird. Wer iSCSI heute ernsthaft betreibt, setzt es in ein eigenes Netzsegment.

Und jetzt die Seite, wegen der ich diese Folge schreibe

Aptio Setup, Driver Health, zwei Treiber als Healthy und zwei als Failed gemeldet

Ganz unten im Advanced-Menü gibt es einen Punkt namens Driver Health. Der sammelt Rückmeldungen aller UEFI-Treiber, die beim Start geladen wurden. Bei mir steht dort:

Intel(R) 10GbE Driver 8.1.00 x64        Healthy
Intel(R) 10GbE Driver 8.1.00 x64        Healthy
Intel(R) 40GbE 1.7.19                   Failed
Intel(R) 40GbE 1.7.19                   Failed
Samsung Electronics NVMe Driver         Healthy

Zweimal Failed, und zwar für meine 10-Gigabit-Steckkarte. Der Klartext dahinter:

The UEFI driver for the device detected a newer version of the NVM image than expected. Please install the most recent version of the UEFI driver.

Diese Meldung ist ein kleines Meisterwerk an Missverständlichkeit, und sie hat eine Vorgeschichte.

Vor einiger Zeit habe ich die Firmware dieser Karte aktualisiert. Sie kam mit NVM 5.05, das war ziemlich alt, und ich habe sie auf 9.56 gebracht. Das ist gut gegangen, die Karte läuft unter Linux tadellos, zehn Gigabit, Flusskontrolle, alles in Ordnung:

ethtool -i ens6f1np1
#   driver: i40e
#   firmware-version: 9.56 0x80010103 17.5.12

Nur besteht so eine Karte aus zwei Teilen Firmware. Da ist das NVM, das den eigentlichen Kartenbetrieb steuert. Und da ist das Option ROM, also der UEFI-Treiber für den Vorstart, den das BIOS ausführt. Ich habe das eine aktualisiert und das andere nicht. Die Zahlen aus dem Menü zeigen den Abstand:

Family Firmware Version           17.5.12
EFI Version                       1.7.19

Der Vorstart-Treiber hat einen fest einkompilierten Versionsbereich, in dem er sein NVM erwartet, und 9.56 liegt darüber. Also meldet er sich krank.

Warum man diese Meldung genau lesen muss

Die naheliegende Reaktion ist, die Firmware der Karte zu aktualisieren. Das wäre falsch, und zwar genau das falsche Ende. Die Meldung sagt nicht „aktualisiere das NVM“, sie sagt install the most recent version of the UEFI driver. Der Treiber beschwert sich über sich selbst, nicht über die Firmware.

Dass wirklich nur der Vorstart betroffen ist, lässt sich schön belegen. Der Linux-Treiber hat dieselbe Prüfung eingebaut, die Meldungstexte stecken im Modul:

strings i40e.ko | grep "newer version of the NVM"
#   The driver for the device detected a newer version of the NVM image v%u.%u than v%u.%u.

Und im Journal steht davon: nichts. Der Kerneltreiber deckt NVM 9.56 ab und ist zufrieden. Es betrifft ausschliesslich Netzwerkstart über diese Karte, und die Maschine startet von der lokalen NVMe.

Was ich damit mache

Es gibt drei Möglichkeiten. Ignorieren, dann bleibt eine rote Zeile auf einer Seite, die ich ohnehin nie öffne. Das Option ROM nachziehen, was einen weiteren erzwungenen Flash auf einer funktionierenden Netzwerkkarte bedeutet, nur um einen Dialog loszuwerden. Oder das Option ROM für diesen Steckplatz einfach abschalten:

Advanced > PCIe/PCI/PnP Configuration
  CPU SLOT6 PCI-E 4.0 X16 OPROM   [Disabled]

Das ist die Variante, die ich wählen würde, wenn es mich stört. Kein Einfluss auf das laufende Netzwerk, denn Linux bringt seinen eigenen Treiber mit. Der einzige Verlust ist PXE-Boot über diese Karte, was ich noch nie gebraucht habe.

Gegen den Nachflash spricht etwas Grundsätzliches: das NVM wurde bereits einmal mit Gewalt überschrieben. Ein zweiter erzwungener Flash auf einer Karte, die einwandfrei arbeitet, ist ein schlechtes Geschäft. Geht er schief, ist die Karte im Vorstart tot, und dann habe ich statt einer kosmetischen Meldung ein echtes Problem.

Übrigens: die Karte taucht in der festen Startreihenfolge gar nicht auf, wohl aber in der manuellen Auswahl. Ihr Option ROM läuft also weit genug, um Startoptionen anzumelden, und meldet sich trotzdem gesundheitlich ab. Funktional fehlt nichts.

Der Message Code, und warum ich ihn in Ruhe lasse

Unter der Meldung steht noch eine Zahl:

Message Code : 9007203549708288

Das sieht nach einem Fehlercode aus, den man nachschlagen könnte. Umgerechnet ist es 0x0020000100000000, also zwei gesetzte Bits in einem 64-Bit-Wert. So glatt ist keine laufende Nummer, das ist ein Bitfeld. Eine öffentliche Tabelle dafür gibt Intel nicht heraus.

Ich schreibe das auf, weil solche Zahlen einen leicht auf eine lange und ergebnislose Suche schicken. Der Klartext darüber ist die Information. Der Zahlenwert ist keine.

Fazit

Fünfzig Bildschirmseiten Netzwerkkonfiguration für eine Maschine, die einen einzigen Anschluss nutzt, sind ein guter Anlass zu fragen, wofür so ein Board eigentlich gebaut wurde. PXE, iSCSI, HTTP-Boot mit Zertifikatsprüfung, das alles ergibt nur Sinn, wenn hunderte gleiche Kisten in einem Rack stehen und keine davon eine Tastatur hat.

Und die Driver Health-Seite ist die vielleicht praktischste Seite im ganzen Setup, weil sie etwas anzeigt, das man sonst nirgends sieht: was die Firmware auf den Steckkarten von sich selbst hält.

Nächste Folge: der Start selbst. Boot-Reihenfolge, UEFI gegen Legacy, und Secure Boot auf einem Board, das noch nie einen Schlüssel gesehen hat.

Siehe auch

Hat jemand von euch schon mal ein Option ROM auf einer Intel-Netzwerkkarte nachgezogen, ohne das NVM anzufassen? Ich hätte gerne einen Erfahrungsbericht bevor ich es selbst probiere, ihr dürft mich fragen.

LoRa-Mesh mit MeshCore, Teil 2: Der erste Companion mit dem Heltec V4

Heltec WiFi LoRa 32 V4 mit MeshCore-Companion-Firmware und angeschlossenem GPS-Modul auf einer blauen Werkbankmatte. Im Hintergrund sind Flash-Befehle, Bluetooth-Kopplung mit dem Smartphone und ein dezentrales LoRa-Mesh-Netzwerk dargestellt.

Im ersten Teil ging es um die Theorie: was LoRa ist, was MeshCore daraus macht und warum ich hier bei MeshCore gelandet bin. Jetzt wird es praktisch. Bevor ein Repeater aufs Dach kommt, braucht man erst einmal ein Gerät, mit dem man selbst im Mesh mitreden kann. Bei MeshCore heißt das Companion: ein kleiner LoRa-Node, der per Bluetooth am Smartphone oder am PC hängt und für dich funkt. Ich habe dafür einen Heltec WiFi LoRa 32 V4 genommen. Es gibt natürlich noch mehr und auch Geräte, die kein Smartphone brauchen aber ich mag es mit dem Smartphone zusammen.

Was in der Box steckt

Mein erstes Board war ein einzelner Heltec V4 mit GPS. In der kleinen Plastikbox lagen die Platine, eine Federantenne mit u.FL-Stecker (oft IPEX genannt), ein GPS-Modul mit achtadrigem Kabel, zwei JST-Kabel für Akku oder Solarzelle und zwei Stiftleisten, die nicht eingelötet sind. Auf der Box steht „LoRa Band, HP 863-928 MHz“, auf der Platine selbst HTIT-WB32LAF V4.3. Das ist die Revision 4.3 mit dem Frontend KCT8103L, also einem zusätzlichen Leistungsverstärker (PA) und rauscharmen Vorverstärker (LNA) vor dem eigentlichen LoRa-Chip SX1262. Der Rechner auf dem Board ist ein ESP32-S3 mit 16 MB Flash und 2 MB PSRAM, dazu kommen ein kleines OLED, USB-C und zwei Taster.

Ausgepackter Heltec WiFi LoRa 32 V4 auf einer blauen Werkbankmatte: Platine mit angeschlossenem GPS-Modul und Federantenne, zwei JST-Kabel mit roten und schwarzen Adern, eine Tüte mit Stiftleisten und die blaue Plastikbox mit der Aufschrift WiFi LoRa 32 V4
Lieferumfang des Heltec V4: Board, GPS-Modul, Federantenne, zwei JST-Kabel und Stiftleisten. Achtung, auf diesem Foto steckt die Antenne noch an der 2,4-GHz-Buchse. Doof ich bin.

Später habe ich mir noch das Komplettset geholt: Heltec V4 ohne GPS, dafür mit fertigem Gehäuse und 3000-mAh-LiPo. Auf dem läuft heute mein persönlicher Node „Kernel-Error“. Wer einfach nur schnell ins Mesh will, ist mit diesem Set besser bedient als ich mit meinem nackten Board, denn Gehäuse und Akku sind dann schon erledigt. Wie der erste Heltec danach ins Auto gewandert ist, steht weiter unten.

Ab Werk war auf dem Board Meshtastic 2.7.22 installiert. Ein meshtastic --info über USB lief sofort. Die Region stand auf UNSET, so sendet das Board gar nicht, bevor man eine Region gesetzt hat. Das finde ich vernünftig, so schreit das Ding nicht schon in die Luft, ganz ohne config. Ich wollte aber MeshCore, also musste die Firmware runter.

Achtung: Der Heltec V4 hat zwei identische IPEX-Buchsen. LoRa ist die Buchse an der Spitze der Platine, auf der Oberseite direkt neben dem GNSS-Stecker. Die andere sitzt unten neben dem ESP32-S3 und gehört zum 2,4-GHz-Teil. Und schick nie ein Paket raus, ohne dass an der LoRa-Buchse eine Antenne oder ein Abschlusswiderstand hängt. Die Sendeleistung muss irgendwo hin, sonst kommt sie zurück und kann die PA beschädigen. Also erst die Antenne, dann USB. Wenn ihr das dann noch direkt richtig anschließt und nicht, wie ich, einfach mal fummelt….

Heltec V4 von oben am USB-C-Kabel, das OLED zeigt Pin: 182366, die Federantenne steckt an der IPEX-Buchse an der Spitze der Platine direkt neben dem farbigen GNSS-Kabel, rechts das grüne GPS-Modul
So ist es richtig: Die Federantenne steckt an der LoRa-Buchse an der Spitze, direkt neben dem GNSS-Kabel. Auf dem Display steht der BLE-PIN fürs Pairing.

Erst sichern, dann flashen

Bevor ich irgendetwas überschreibe, ziehe ich ein komplettes Abbild des Flashs. Damit komme ich jederzeit zurück auf den Werkszustand. Klar könnte ich einfach wieder die Meshtastic Firmware drüber brennen. Ich bin aber schon ein paar mal mit so etwas auf die Nase gefallen und die drei Minuten für eine Sicherung habe ich dann doch. Der ESP32-S3 meldet sich über seinen nativen USB-Port als /dev/ttyACM0, einen USB-Seriell-Wandler gibt es auf dem Board nicht. Mit esptool schaue ich zuerst, was überhaupt verbaut ist, und lese dann die vollen 16 MB aus. Das hat bei mir knapp 100 Sekunden gedauert.

$ esptool --port /dev/ttyACM0 flash-id
Chip type:          ESP32-S3 (QFN56) (revision v0.2)
Features:           Wi-Fi, BT 5 (LE), Dual Core + LP Core, 240MHz, Embedded PSRAM 2MB (AP_3v3)
Detected flash size: 16MB

$ esptool --port /dev/ttyACM0 --baud 921600 read-flash 0 ALL backup/heltec-v4-factory-16MB.bin

Die Zeile mit dem PSRAM ist wichtig für die Wahl der richtigen Firmware. MeshCore baut für den V4 mehrere Varianten: heltec_v4, heltec_v4_r8 und jeweils noch _tft-Versionen. Das „R2“ im Produktnamen steht für den ESP32-S3R2 mit 2 MB PSRAM, und das bestätigt esptool. Also heltec_v4. Die r8-Variante benutzt andere Pins für Display-Versorgung und LED, mit dem falschen Image bleibt das Display dunkel. Ratet doch mal, woher ich das so genau sagen kann? Nicht, dass noch das Gefühl aufkommt, dass ich hier Ahnung von dem hätte, was ich mache. Wie immer: Korrigiert mit bitte. Ich will was lernen.

Die Releases auf GitHub sind nach Rolle getrennt: Companion, Repeater und Room Server haben eigene Tags. Für das Handgerät brauche ich den Companion mit Bluetooth, also eine Datei nach dem Muster heltec_v4_companion_radio_ble-<version>-merged.bin. „merged“ heißt, dass Bootloader, Partitionstabelle und Anwendung in einem Image stecken, das ab Adresse 0x0 geschrieben wird. Die SHA-256-Summe habe ich gegen den Digest des GitHub-Assets geprüft, dann den Flash gelöscht, damit keine Meshtastic-Reste liegen bleiben, und das Image geschrieben (vergesst das mit dem Löschen nicht, kommt sonst schnell mal spannendes bei herum):

$ gh release download companion-v1.17.1 -R meshcore-dev/MeshCore -p 'heltec_v4_companion_radio_ble-v1.17.1-d929643-merged.bin'
$ sha256sum heltec_v4_companion_radio_ble-v1.17.1-d929643-merged.bin
$ esptool --port /dev/ttyACM0 erase-flash
$ esptool --port /dev/ttyACM0 --baud 921600 write-flash 0x0 heltec_v4_companion_radio_ble-v1.17.1-d929643-merged.bin

Die 1,3 MB waren in unter fünf Sekunden drauf. Wer keine Lust auf die Kommandozeile hat, kann auch den Web-Flasher von MeshCore im Browser nehmen. Den habe ich selbst nicht benutzt, das Backup vorher solltest du aber in beiden Fällen machen.

Eine Sache aus dem Quellcode der V4-Variante muss man kennen: Dort steht LORA_TX_POWER=10 mit dem Kommentar, dass daraus am Ende 22 dBm werden. Die Sendeleistung in der App ist auf diesem Board die Ausgangsleistung des SX1262, die externe PA legt laut diesem Kommentar etwa 12 dB obendrauf. Eine „22“ in der App bedeutet beim Heltec V4 also nicht 22 dBm an der Antenne. Mein Companion bleibt deshalb auf 10, dem Wert, den MeshCore für den V4 ohnehin mitbringt. Was das für die Grenzwerte heißt, rechne ich in Teil 4 durch. Ja, hehe… Der cliffhanger bleibt auch hier.

Pairing mit der App

Nach dem Flashen bootet das Board direkt in MeshCore und zeigt auf dem OLED einen sechsstelligen PIN. Den gibt man beim Bluetooth-Pairing in der MeshCore-App ein. Der PIN wird bei jedem Start neu gewürfelt. Der Companion nimmt immer nur einen BLE-Client gleichzeitig an. Hängt das Handy dran, findet der PC das Gerät nicht, und umgekehrt. Was mich bestimmt 10 min. meines Lebens gekostet hat, das zu realisieren.

Beim ersten Verbinden fragt die App nach einem Funk-Preset. Hier im Rheinland funkt die Community mit „EU/UK (Narrow)“, das sind 869,618 MHz, 62,5 kHz Bandbreite, Spreading Factor 8 und Coding Rate 8. Welches Preset in deiner Gegend gilt, solltest du bei der lokalen Community nachschauen, denn mit anderer Frequenz, Bandbreite oder anderem Spreading Factor hörst du schlicht niemanden. Die Coding Rate steht dagegen im LoRa-Header, ein Empfänger mit Coding Rate 5 versteht also auch Pakete mit Coding Rate 8. Danach bekommt der Node einen Namen. Meiner heißt „Kernel-Error“.

MeshCore-App, Einstellungen des Nodes Kernel-Error: Avatar mit dem Buchstaben K, darunter der verkürzte Schlüssel b2035736...84ce8610, Felder für Name und öffentlichen Schlüssel, Breiten- und Längengrad 0.0, Häkchen Position in Advert teilen nicht gesetzt, Preset EU/UK (Narrow) mit 869.618 MHz und 62.5 kHz, unten ein Info-Hinweis zur Iconfarbe
Die Node-Einstellungen in der App. Unter dem Namen steht die Kurzform des öffentlichen Schlüssels, die Position wird nicht im Advert geteilt.

Erstes Advert, erste Nachrichten

Ein Advert ist die Visitenkarte eines Nodes: Name, öffentlicher Schlüssel, Typ und optional eine Position, signiert mit dem privaten Schlüssel, aber nicht verschlüsselt. Ein Companion verschickt so etwas nur, wenn du es auslöst. Als Flood-Advert geht es über die Repeater in die Fläche, als Zero-Hop-Advert erreicht es nur die direkte Nachbarschaft.

Nach meinem ersten Flood-Advert war die Kontaktliste leer. Das ist normal und hat mich trotzdem kurz irritiert. Ein Advert holt keine Liste aus dem Netz, Kontakte tauchen erst auf, wenn du die Adverts der anderen hörst, und Repeater senden die nur in längeren Abständen. Etwas Geduld hilft also. Bei mir hat es zusätzlich etwas länger gedauert, weil die Antenne an der falschen Buchse steckte, siehe oben. Ja, ich bin einer von denen, die auf dem ersten Foto die Antenne an der falschen Buchse hatten.

Ob eine Nachricht wirklich rausgegangen ist, zeigt dir die App selbst. Unter jeder eigenen Kanalnachricht zeigt sie an, wie viele Repeater sie wiederholt haben. Hört dein Companion einen Repeater deine Nachricht weitersenden, ist sie im Mesh. Diese „heard repeats“ empfiehlt auch Mesh Rheinland zum Testen. Bei mir stand unter dem ersten „geht“ im Public-Kanal „1 Repeater empfangen“, drumherum liefen Testnachrichten aus Velbert, Euskirchen und Lengerich.

MeshCore-App im Kanal Public: Nachrichten anderer Nutzer wie Test aus velbert und Test aus Euskirchen, rechts die eigene blaue Nachricht @DE_NW_SWE01 geht mit dem Vermerk 1 Repeater empfangen und einem Häkchen, unten das Eingabefeld mit 0/130 Zeichen
Der Moment, in dem es funktioniert: „1 Repeater empfangen“ unter der eigenen Nachricht heißt, dass ein Repeater sie weitergesendet hat.

Avatar per Emoji

Ein Profilbild kann man nicht hochladen. Die App erzeugt die Farbe des Icons aus dem Namen und zeigt den ersten Buchstaben. Steht am Anfang des Namens ein Emoji, nimmt die App das statt des Buchstabens. Ich habe mich zuerst „😈 Kernel-Error“ genannt, das kommt dem FreeBSD-Daemon am nächsten, einen echten Beastie gibt es als Emoji leider nicht. Falls jemand ein Unicode-Zeichen für Beastie kennt, sagt gerne Bescheid, ich schaue dann nach, ob die App es mitmacht. Der Name darf maximal 31 Byte lang sein, ein Emoji frisst davon meist vier. Nach dem Umbenennen ein Flood-Advert schicken, damit die anderen den neuen Namen lernen. Am Schlüssel ändert sich nichts. Das kleine OLED kann übrigens keine Emojis darstellen und zeigt nur den Text.

MeshCore-App, Kontaktliste mit 32 Einträgen, oben in der Kopfzeile 100 Prozent Akku und der Name Kernel-Error mit Teufels-Emoji, darunter Repeater wie DE-NW-Wpt.-Vohwinkel LZ, DE-NW-BM-GlessenerHoehe, DE-NW-Wuppertal-West und der Raum Hi @ meshrheinland.de, jeweils mit verkürztem Schlüssel und Uhrzeit
Nach ein paar Stunden füllt sich die Kontaktliste. Oben steht der Node mit Emoji im Namen.

Kontakte und warum der Schlüssel zählt

Wie in Teil 1 beschrieben, ist die Identität eines Nodes sein Schlüsselpaar und der Name nur ein Etikett. Die App zeigt deshalb neben jedem Kontakt eine Kurzform des öffentlichen Schlüssels, die ersten und letzten acht Hex-Zeichen, bei mir <b2035736...84ce8610>. Wenn du sicher sein willst, dass du wirklich mit der Person schreibst, die du meinst, vergleich diesen Fingerprint über einen anderen Weg: persönlich, am Telefon oder über eine Webseite, der du vertraust. Erst dann ist der Kontakt verifiziert.

Standardmäßig wird jedes gehörte Advert zum Kontakt. Das wird schnell viel: Die Kontakttabelle ist auf dem Heltec-BLE-Build auf 350 Einträge begrenzt, und im Rheinland gibt es viele Repeater. Ist die Tabelle voll und „älteste überschreiben“ aus, landen neue Kontakte nicht mehr in der Liste. Die Firmware reicht das Advert dann zwar noch an die App weiter und meldet, dass der Speicher voll ist, speichert den Kontakt aber nicht. Ich habe deshalb auf selektives Hinzufügen umgestellt: Chats, Repeater und Room Server ja, Sensoren nein, maximal drei Hops entfernt, älteste überschreiben an. Favoriten werden beim Überschreiben nicht angefasst, die eigenen wichtigen Kontakte sollte man also als Favorit markieren.

Die App kann die komplette Node-Konfiguration als JSON exportieren und wieder importieren. Achtung: In diesem Export steckt der private Schlüssel, und dazu die zuletzt bekannte Position, auch wenn du sie nicht im Advert teilst. Behandle die Datei wie einen privaten SSH-Schlüssel. Wer ihn hat, kann sich als dein Node ausgeben und deine Direktnachrichten lesen. Hier deshalb nur ein bereinigter Auszug mit den Teilen, die wirklich etwas einstellen:

"radio_settings": {
  "frequency": 869618,
  "bandwidth": 62500,
  "spreading_factor": 8,
  "coding_rate": 8,
  "tx_power": 10
},
"other_settings": {
  "manual_add_contacts": 1,
  "advert_location_policy": 0
},
"auto_add_settings": {
  "auto_add_chat": true,
  "auto_add_repeater": true,
  "auto_add_room_server": true,
  "auto_add_sensor": false,
  "overwrite_oldest": true,
  "auto_add_max_hops": 4
}

manual_add_contacts: 1 schaltet das selektive Hinzufügen ein, erst dann wirken die auto_add_*-Schalter. auto_add_max_hops: 4 bedeutet bis zu drei Hops, 0 hieße ohne Grenze. advert_location_policy: 0 hält die Position aus dem Advert raus. Ein Companion, der meistens zu Hause liegt, würde sonst die Wohnadresse ins Netz senden. Wenn du so etwas übernehmen willst, setz die Werte in der App oder kopier nur diese Blöcke. Einen fremden Export mitsamt Schlüssel zu importieren, klont eine fremde Identität, und zwei Nodes mit demselben Schlüssel bringen das Routing für beide durcheinander.

Die Einstellungen, auf die es ankommt

Zwei Einstellungen stehen nicht im Export und müssen von Hand gesetzt werden: die Größe der Pfad-Hashes und der Default-Scope. Den Pfad-Hash habe ich auf 2 Byte gestellt, damit sich Repeater auf dem Weg seltener verwechseln lassen. Der Default-Scope steht auf #de, weil die Repeater hier ungescopte Floods ignorieren. Beides erkläre ich ausführlich in Teil 6. Die Funkwerte entsprechen dem Preset, zur Sendeleistung von 10 steht oben schon alles. Ein Board ohne externe PA würde man dagegen mit seiner echten Ausgangsleistung einstellen.

MeshCore-App, Einstellungen des Companions: öffentlicher Schlüssel, Breiten- und Längengrad 0.0, Position in Advert teilen deaktiviert, Preset EU/UK (Narrow), Frequenz 869.618 MHz, Bandbreite 62.5 kHz, Spreading-Faktor 8, Codierungsrate 8, Sendeleistung 10 dBm, Repeat-Modus deaktiviert, Default Path Hash Size 2-Byte (max. 32 Hops), Default Region Scope #de
Die Companion-Einstellungen von Kernel-Error: Sendeleistung 10, Pfad-Hash 2 Byte, Default-Scope #de, Position im Advert aus und Repeat-Modus aus.

Der Schalter „Repeat-Modus aktivieren“ sieht verlockend aus, macht aber nicht, was man erwartet. Laut Quellcode der Companion-Firmware ist er nur auf 433,000, 869,495 oder 918,000 MHz erlaubt. Auf 869,618 MHz lehnt die Firmware ihn ab. Wer ihn einschaltet, zieht also auf eine andere Frequenz um und verlässt das Netz der Community. Gedacht ist das für Off-Grid-Gruppen ohne Repeater. Bei meinen Companions bleibt er aus, das Weiterleiten erledigen hier die Repeater.

Kanäle

Neben Direktnachrichten an Kontakte gibt es Kanäle. Jeder MeshCore-Companion kennt den Kanal „Public“. Dessen Schlüssel 8b3387e9c5cdea6ac9e5edbaa115cd72 ist überall gleich und öffentlich, die Verschlüsselung schützt dort also vor niemandem im Mesh. Dazu kommen Hashtag-Kanäle wie #nrw, #bonn oder #rheinbach. Bei denen wird der Schlüssel per SHA-256 aus dem Namen abgeleitet. Wer den Namen kennt, kann mitlesen. Privat sind nur Kanäle mit einem selbst erzeugten, geheim geteilten Schlüssel.

Jeder Kanal kann zusätzlich einen eigenen Scope tragen, meinen Public-Kanal habe ich auf de gestellt. Welcher Scope für welchen Kanal sinnvoll ist und wie du den tatsächlich wirksamen Scope prüfst, steht in Teil 6. Ein Nebeneffekt, den man sofort sieht: Mit Scope begrenzt die App die maximale Nachrichtenlänge im Kanal auf 120 statt 130 Zeichen. Die Firmware selbst kürzt dabei nichts, der Transport-Code steckt im Paketkopf und nicht in den Nutzdaten.

Mein Kontakt als QR-Code

Über „Kontakt teilen“ erzeugt die App einen QR-Code. Darin steckt eine Adresse der Form meshcore://contact/add?name=...&public_key=...&type=1, also nur Name, öffentlicher Schlüssel und Typ (1 steht für Chat). Position, Zeitstempel und Signatur fehlen. Der Code beweist deshalb nicht von selbst, dass er von mir stammt. Das Vertrauen kommt hier aus dem Kanal: Du hast ihn aus meinem Blog. Wenn du mich hinzufügen willst, scann den Code und vergleich den Fingerprint mit <b2035736...84ce8610>. Der volle Schlüssel lautet:

b203573688347b345a560e0a8156ecc6c0dc7d14c4f53046064bfabe84ce8610
Schwarz-weißer QR-Code mit einer MeshCore-Kontaktadresse, die den Namen Kernel-Error und den öffentlichen Schlüssel enthält
Mein Companion „Kernel-Error“ als MeshCore-Kontakt. Der Code enthält nur Namen und öffentlichen Schlüssel.

Viel gebe ich damit nicht preis. Name und Schlüssel stehen ohnehin in jedem Advert, das mein Node sendet. Was dazukommt, sind vielleicht ein paar ungefragte Direktnachrichten. Die nehme ich in Kauf.

Ein Gehäuse aus dem Drucker

Mein erstes Board lag anfangs nackt auf dem Tisch. Für unterwegs habe ich mir das Modell „WiFi LoRa 32 V4 (Heltec V4)“ von Adi_CEvil auf MakerWorld in Rot gedruckt. Danke dafür. Heltec und ein 1100-mAh-LiPo passen hinein, geladen wird über USB-C. Beim Filament lieber keine Varianten mit Carbon- oder Metallanteil nehmen, leitfähige Füllstoffe direkt an der Antenne verstimmen sie und schlucken Leistung. Ihr wickelt das Teil ja auch nicht in Alufolie ein, oder?

Rotes 3D-gedrucktes Gehäuse mit Heltec V4 und Akku auf einer Werkbankmatte, im Fenster das OLED mit Kernel-Error, Akkusymbol, MSG: 0 und Pin: 370062, oben rechts schaut die schwarze Federantenne heraus
Fertig für die Hosentasche: Heltec V4 mit LiPo im gedruckten Gehäuse. Das OLED zeigt den Namen ohne Emoji, der kleine Font kennt keine Emojis.

Vom Linux-Rechner aus: meshcore-cli und Meshy

Am PC benutze ich zwei Werkzeuge. Für die Kommandozeile gibt es meshcore-cli, installiert mit pipx, der Befehl heißt dann meshcli. Gekoppelt wird über BlueZ. Vorher am Handy Bluetooth aus, denn es gibt nur den einen BLE-Platz. Nach dem Pairing hält bluetoothctl die Verbindung gern offen, dann findet meshcli das Gerät nicht. Ein disconnect löst das.

$ pipx install meshcore-cli
$ bluetoothctl
[bluetooth]# agent KeyboardOnly
[bluetooth]# pair <BLE-MAC>
  (PIN vom OLED eingeben)
[bluetooth]# trust <BLE-MAC>
[bluetooth]# disconnect <BLE-MAC>
$ meshcli -a <BLE-MAC> ver
Model: Heltec V4.3 OLED

Mit meshcli komme ich an Dinge, die die App nicht zeigt, etwa die Funkstatistik mit get stats_radio oder node_discover, bei dem Repeater in direkter Reichweite mit ihren Empfangswerten antworten. Den Pfad-Hash kann man dort mit set path_hash_mode 1 auf 2 Byte stellen. Ein Tipp aus der Praxis: meshcli 1.6.5 holt die Kontakte mit einem Timeout von 5 Sekunden. Bei über hundert Kontakten über BLE reicht das nicht, dann behauptet das Tool, es gäbe keine. Die Python-Bibliothek dahinter mit längerem Timeout schafft es.

Für den Alltag am Schreibtisch nehme ich Meshy, einen grafischen MeshCore-Client für den Linux-Desktop. Meshy verbindet sich ebenfalls per BLE, zeigt Geräteinfos, Kontakte, Kanäle und eine Karte und kann Adverts senden, Pfade tracen und Nodes in der Nähe suchen.

Meshy unter Linux, Ansicht Device Information: Status Bluetooth Connected, Node Name Kernel-Error, Board Heltec V4.3 OLED, Firmware v1.17.1-d929643 vom 14-Aug-2026, öffentlicher Schlüssel, Location GPS off, Battery 97 % bei 4139 mV, links Aktionen wie Send Advert, Trace Path, Discover Nearby Nodes und Rx Log
Meshy unter Linux: Geräteinfo des Companions, hier noch mit Firmware v1.17.1.
Meshy-Kontaktliste mit 150 Kontakten, oben ErrorCar als Favorit mit Direct-Route, darunter Repeater wie DE-NW-SU-Todenfeld-KE, DE-NW-MH-CITYRPT, DE-NW Graniti Duisdorf und DE-RP-Remagen mit Flood-Route
Kontakte in Meshy. Favoriten stehen oben, daneben steht, ob der Weg bekannt ist (Direct) oder geflutet wird (Flood).
Meshy mit geöffnetem Kanal Public, links die Kanalliste mit Public, #linux, #nrw, #bonn, #rheinland, #wetter, #rheinbach, #todenfeld und #goch, rechts zwei Nachrichten mit den Angaben 4 hops und de sowie 8 hops
Der Public-Kanal in Meshy. Unter jeder Nachricht stehen Hop-Anzahl und, falls vorhanden, der Scope.

KiekR auf Android

Auf dem Handy bin ich inzwischen bei KiekR gelandet, einer alternativen Android-App von Marcel Verdult (kiekr.app). Sie ist Closed Source, das sollte man wissen. Mir gefällt sie trotzdem besser als die offizielle App, weil sie mehr vom Funk zeigt: den Scope pro Nachricht, eine Statistik und vor allem „Live-Funk“, eine Rohansicht aller empfangenen Pakete, auch der, die der Node nicht entschlüsseln kann. KiekR ersetzt die offizielle App, parallel geht wegen des einen BLE-Platzes nicht.

KiekR-Chatliste mit Kanälen #nrw, #bochum, #bonn, #eifel, #euskirchen, #goch, #hamradio, #kevelaer und #kleve, Nachrichtenvorschauen unscharf gemacht, oben ein Hinweis Neue Kanäle in der Nähe gehört: #duisburg
Die Chatliste in KiekR. Die App meldet sogar Hashtag-Kanäle, die in der Nähe gehört wurden.

An einem Morgen zu Hause hat der Live-Funk innerhalb weniger Minuten 250 Pakete gezeigt, rund 23 pro Minute. 232 davon waren für meinen Node nicht lesbar. Das ist kein Fehler, das sind Direktnachrichten, Anfragen und Antworten anderer Nodes und Kanäle, deren Schlüssel ich nicht habe. Schön zu sehen war auch, wie weit gescopte Floods laufen: Eine Anfrage mit Scope de kam über 17 und 18 Hops, mit 1-Byte-Pfad-Hashes. Jeder dieser Hops kostet Sendezeit. Warum das relevant ist, steht in Teil 6.

KiekR Live-Funk ungefiltert: 250 Pakete gesamt, Rauschen -89 dBm, Mesh ausgelastet mit 23 pro Minute, 236 Multi-Hop, 232 undekodierbar, darunter TFlood-REQ-Pakete mit 17 und 18 Hops, Scope de und Pfad mit 1 Byte pro Hop, Hinweis verschlüsselt, nicht für dieses Gerät
Live-Funk in KiekR: Die meisten Pakete sind verschlüsselt und nicht für diesen Node. Oben ein gescopter Flood über 17 Hops.
KiekR-Statistik: 336 Adverts gehört, 266 in den letzten 7 Tagen, 319 mit GPS, 336 Kontakte installiert, nach Typ 34 Chats, 292 Repeater, 10 Räume, 0 Sensoren, 0 Andere
336 Kontakte bei einem Limit von 350: Ohne „älteste überschreiben“ wäre die Tabelle bald voll.

KiekR hat außerdem optionale Community-Funktionen, etwa das Hochladen gehörter Repeater-Adverts auf Karten oder einen Observer-Modus, der Rohpakete an Analyse-Seiten weiterreicht. Die sind standardmäßig aus. Bevor du sie auf einem mobilen Node einschaltest, überleg kurz, was sie über deine Wege verraten.

Firmware-Update ohne Datenverlust

Mitten im Projekt erschien MeshCore v1.18.0 (Release-Notes). Unter anderem kann der BLE-Build für den Heltec V4 seitdem auch WLAN. Beim Update will ich weder Identität noch Kontakte verlieren, also gehe ich so vor:

  • Erst wieder ein volles 16-MB-Backup mit read-flash. Darin steckt der private Schlüssel, die Datei bekommt Rechte 600 und bleibt auf dem eigenen Rechner.
  • Die Partitionstabelle aus dem Backup lesen. Bei mir liegt die Anwendung ab 0x10000, Identität, Kontakte, Kanäle und Einstellungen liegen in einer eigenen Datenpartition weiter hinten.
  • Prüfen, ob Bootloader und Partitionstabelle im neuen Merged-Image identisch sind. Nur dann reicht es, das reine App-Image an 0x10000 zu schreiben.
  • Kein erase-flash. Damit bleiben Datenpartition und BLE-Kopplung unangetastet.
$ esptool --port /dev/ttyACM0 --baud 921600 read-flash 0 ALL backup/companion-before-1.18.0-16MB.bin
$ chmod 600 backup/companion-before-1.18.0-16MB.bin
$ esptool --port /dev/ttyACM0 --baud 921600 write-flash 0x10000 heltec_v4_companion_radio_ble-v1.18.0-81dbae5.bin

Das Schreiben hat acht Sekunden gedauert. Danach meldete sich der Node mit v1.18.0-81dbae5, gleichem Schlüssel, gleichem Namen, allen 244 Kontakten, allen Kanälen, 2-Byte-Pfad-Hash und Default-Scope #de. Auch das Pairing mit dem Handy hielt. Nur die Uhr lief nach den Resets ein paar Minuten hinterher. Nach einem Update also kurz die Zeit vom Handy oder PC synchronisieren. Keine Angst, hier fühlt man sich ganz schnell wieder zuhause, wenn man schon mal mit arduino, esp usw. gearbeitet hat.

MeshCore-App, Einstellungsmenü mit App-Datenbank exportieren, Daten löschen, Fehlerbericht, Debug-Protokolle, Kommandozeile, Auf Werkseinstellungen zurücksetzen und Neustart, darunter Geräte-Info mit Kanäle 1/40, Kontakte 163/350, Speicher 3 % belegt, Geräte-Modell Heltec V4.3 OLED, Firmware-Datum 15-Oct-2026, Firmware-Version v1.18.0-81dbae5, Connected via BLE
Geräte-Info in der App nach dem Update auf v1.18.0.

Ein zweiter Node mit GPS fürs Auto

Als das Komplettset da war, habe ich meine Identität „Kernel-Error“ auf das neue Board umgezogen. SUPER WICHTIG: Ein Schlüssel gehört nur einem aktiven Gerät. Also zuerst das alte Board komplett löschen und neu flashen, dann auf dem neuen Board den gesicherten Export importieren. Pfad-Hash und Default-Scope stehen nicht im Export und müssen auf beiden Geräten neu gesetzt werden.

Das alte Board mit dem GPS-Modul hat einen neuen Schlüssel bekommen und wohnt jetzt als „ErrorCar“ im Auto. Die Position sendet es nicht im Advert. Stattdessen darf genau ein Kontakt, mein persönlicher Node, sie per Telemetrie abfragen. Die Antwort ist verschlüsselt, Repeater unterwegs sehen nur Chiffretext, und ohne Anfrage verrät das Auto gar nichts. Achtung: Das klappt nur, wenn neben der Standort-Telemetrie auch die Basis-Telemetrie für diesen Kontakt freigegeben ist, sonst antwortet der Node niemandem. Was ich mit dem Auto-Node sonst noch mache, zeige ich in Teil 5 über MeshMapper. Aber ich werde an dem Punkt nicht der Einzige sein, der den Satz „ich kann remote also die GPS Position abfragen?“ im Kopf hatte. Damit ist auch klar, warum ich an Sensoren denke und öhm einen GPS Tracker?!

Was du jetzt tun kannst

Wenn du selbst einsteigen willst, ist das Heltec-V4-Komplettset mit Gehäuse und 3000-mAh-Akku der bequemste Weg: Antenne an die LoRa-Buchse, Backup ziehen, MeshCore drauf, Preset deiner Region wählen, Advert senden und im Public-Kanal auf das erste „Repeater empfangen“ warten. Wer gleich einen Companion fürs Auto oder ein zweites Board für einen eigenen Repeater haben möchte, bekommt den Heltec V4 mit L76K-GPS im Doppelpack. Eins solltest du dabei wissen: Der V4 ist ein ESP32-S3 mit zusätzlichem Verstärker und braucht deutlich mehr Strom als Boards mit nRF52-Chip, etwa der RAK4631. Für meinen Einstieg war mir das egal. Wer lange am Akku laufen will, schaut sich besser so ein Board an. Und wenn du mich hinzufügst, freue ich mich über ein kurzes „Moin“ per Direktnachricht.

Siehe auch: Teil 1: LoRa und MeshCore, Teil 3: Der Repeater, Teil 4: Antennen und Messen, Teil 5: MeshMapper, Teil 6: Konfiguration, die zählt.

Links zu Amazon und AliExpress sind Affiliate-Links: Kaufst du darüber, bekomme ich eine kleine Provision, für dich ändert sich am Preis nichts. Als Amazon-Partner verdiene ich an qualifizierten Verkäufen.

Wenn ihr Fragen habt oder euer Companion partout keinen Repeater hören will, dann dürft ihr mich gerne fragen.

LoRa-Mesh mit MeshCore, Teil 1: LoRa, Routing und warum MeshCore

Heltec WiFi LoRa 32 V4 mit MeshCore-Firmware und GPS-Modul vor einer Landschaft im Rheinland. Funkverbindungen zwischen mehreren Repeatern veranschaulichen ein dezentrales LoRa-Mesh-Netzwerk ohne Internet oder Mobilfunk.

Textnachrichten über zig Kilometer verschicken, ohne Mobilfunk, ohne Internet und ohne dass irgendwo ein Server stehen muss. Mit ein paar Euro Hardware und einer ordentlichen Antenne geht das inzwischen ziemlich gut. Die Technik dahinter heißt LoRa, und darauf laufen verschiedene Mesh-Projekte. Ich habe mich hier für MeshCore entschieden und betreibe inzwischen einen festen Repeater auf dem Dach, einen Node fürs Handy und einen fürs Auto. Weil… Weil ich wohl noch immer ein Tech Nerd bin, den man mit so etwas bekommt. Dezentral, offen, community und nützlich!

Daraus ist wieder mal eine kleine Serie geworden. Die Teile sind geschrieben und die Veröffentlichung ist wieder wöchentlich geplant, wie auch bei der Bios Serie. Das klappt ja so ganz gut, nur manche Links auf die Folgebeiträge sind damit eher eine Art Cliffhanger geworden; was aber auch zu einer Serie passt, oder? Dieser erste Teil erklärt, wie so oft, die Grundlagen: was LoRa auf der Funkseite macht, welche Regeln im 868-MHz-Band gelten, wie MeshCore Nachrichten durch das Netz bringt und warum ich nicht beim vorinstallierten Meshtastic geblieben bin. Am Ende findest du eine Übersicht über alle sechs Teile.

Heltec WiFi LoRa 32 V4 am USB-C-Kabel, das OLED zeigt den MeshCore-Schriftzug, https://meshcore.io und die Firmware v1.17.1, rechts hängt ein L76K-GPS-Modul am Flachbandkabel
Der erste Start: ein Heltec V4 mit frisch geflashter MeshCore-Firmware v1.17.1

Was LoRa ist

LoRa (Long Range) ist ein Funkverfahren von Semtech. Es ist nur die physikalische Schicht, also wie Bits über die Luft gehen. LoRaWAN ist etwas anderes: ein Netzwerkprotokoll mit Gateways und zentralen Servern, wie man es von Sensoren und TTN kennt. MeshCore und Meshtastic nutzen dagegen nur die LoRa-Modulation und bauen ihr eigenes Mesh darauf.

Technisch ist LoRa ein Chirp-Spread-Spectrum-Verfahren. Ein Symbol ist ein Chirp, also ein Ton, dessen Frequenz gleichmäßig über die gesamte Bandbreite läuft. Die Information steckt darin, an welcher Stelle der Chirp startet. Der Empfänger korreliert gegen diese bekannte Form und kann ein Signal noch dekodieren, wenn es deutlich unter dem Rauschen liegt. Daher kommt die Reichweite. Wie sich das im Detail verhält, legen drei Parameter fest:

  • Spreading Factor (SF) legt fest, wie viele Bits ein Symbol trägt, von SF5 bis SF12. Ein Symbol besteht aus 2^SF Chips. Jede Stufe höher verdoppelt die Symboldauer, das Signal wird robuster und lässt sich bei schlechterem Signal-Rausch-Abstand noch empfangen, braucht aber doppelt so lange.
  • Die Bandbreite (BW) sagt, wie breit der Chirp ist, typisch 62,5, 125 oder 250 kHz. Schmaler heißt langsamer, aber auch weniger Rauschen im Empfänger.
  • Die Coding Rate (CR) ist der Anteil der Vorwärtsfehlerkorrektur. Bei 4/5 kommen auf vier Nutzbits fünf gesendete, bei 4/8 acht. Mehr Redundanz kostet Sendezeit, verzeiht dafür kaputte Bits.

Bei SF8 und 62,5 kHz dauert ein Symbol 2^8 / 62.500 Hz, also rund 4,1 ms. Daraus ergibt sich die Airtime, die Zeit, die ein Paket den Kanal belegt. Und die ist bei LoRa die knappe Ressource. Es gibt nur einen Kanal, alle teilen ihn sich, und jede Millisekunde, in der ein Node sendet, kann kein anderer in Reichweite sinnvoll senden. Ein Mesh, in dem jeder Node alles wiederholt, läuft deshalb schnell voll. Zugegeben, dass löst flashbacks aus, in eine Zeit in welcher ich mich mit BNC Netzwerken beschäftigen „durfte“.

Der Rahmen: 869,4 bis 869,65 MHz

In Europa funken MeshCore und Meshtastic im 868-MHz-Bereich. Dafür braucht man keine Lizenz, muss sich aber an die Allgemeinzuteilung halten. Für das Teilband, in dem die Community hier unterwegs ist, gelten laut BNetzA Vfg 91/2025 (Band 54, entspricht ERC REC 70-03) diese Eckdaten (die werden wir später noch wieder sehen und immer mal wieder kontrollieren, dass wir uns daran auch sicher halten):

  • Frequenzbereich 869,4 bis 869,65 MHz
  • maximal 500 mW ERP, das sind 27 dBm
  • höchstens 10 % Duty Cycle, also maximal 360 Sekunden Sendezeit pro Stunde und Gerät

ERP heißt Effective Radiated Power und bezieht sich auf die Abstrahlung einer Dipolantenne. Wichtig ist: Der Antennengewinn zählt mit. Weil ERP gegen den Dipol rechnet, gehen vom dBi-Wert 2,15 dB ab. Wer am Board 27 dBm einstellt und dann eine Antenne mit mehr als 2,15 dBi Gewinn anschraubt, liegt drüber. Kabelverluste ziehen wieder etwas ab. Beim Heltec V4 mit seinem zusätzlichen Leistungsverstärker ist das kein theoretisches Thema, mit einer Außenantenne kommt man auf dem Papier schnell an die Grenze. Wie man das ausrechnet und warum Messen mit Hobby-Equipment schwierig ist, steht ausführlich in Teil 4.

Die 10 % Duty Cycle klingen nach viel. Bei Paketen, die eine Drittelsekunde bis eine ganze Sekunde dauern, ist ein viel genutzter Repeater aber schneller am Limit, als man denkt. Airtime zählt also zweimal, einmal auf dem gemeinsamen Kanal und einmal im eigenen Budget. Klingt schon fast wie Afu (Amateurfunk) und ja man beschäftigt sich zum Teil mit den gleichen Dingen und Physik.

Das Preset hier in der Region

Damit sich Nodes überhaupt hören, müssen Frequenz, Bandbreite, SF und CR exakt übereinstimmen. Die Community im Rheinland nutzt das Preset „EU/UK (Narrow)“. Bei mir sind das diese konfigurierten Werte:

Frequenz:    869.618 MHz
Bandbreite:  62.5 kHz
SF:          8
CR:          8   (= 4/8)

Nominell belegt der Kanal damit 869,618 MHz plus/minus 31,25 kHz, also 869,58675 bis 869,64925 MHz. Das liegt im Teilband, bis zur oberen Kante bleiben aber nur 750 Hz. Das ist die rechnerische Spanne aus Mittenfrequenz und Bandbreite, keine Messung der tatsächlichen Aussendung.

Schmale Bandbreite mit SF8 heißt nicht automatisch kurze Pakete. Ich habe das mit der Time-on-Air-Formel aus dem SX126x-Datenblatt nachgerechnet (CRC an, expliziter Header, für MeshCore 32 Symbole Präambel, die die Firmware bei SF8 setzt, für LongFast 16), daneben das Meshtastic-Standardpreset LongFast:

NutzlastMeshCore SF8 / 62,5 kHz / CR 4/8SF8 / 62,5 kHz / CR 4/5Meshtastic LongFast SF11 / 250 kHz / CR 4/5
20 Byte378 ms304 ms395 ms
50 Byte607 ms447 ms641 ms
100 Byte1033 ms714 ms1010 ms
180 Byte1689 ms1123 ms1583 ms

Die Airtime liegt also ungefähr auf dem Niveau von LongFast, mit CR 4/8 etwas darüber. CR 4/8 bringt Robustheit und bezahlt sie mit Sendezeit. Auch die theoretische Empfindlichkeit ist ähnlich, rund -130 dBm für SF8 bei 62,5 kHz gegenüber rund -131,5 dBm für SF11 bei 250 kHz. Beides sind gerechnete Schätzwerte, keine Messungen. Ich bin auch nur eine Wurst, korrigiert mich also bitte unbedingt. Einfach als Kommentar, oder ihr erreicht mich über einen anderen Weg, vielleicht sogar direkt über das MeshCore Netz? So kann ich lernen, besser werden, euch dankbar sein und es hier korrigieren.

Was MeshCore ist

MeshCore ist eine Open-Source-Firmware für LoRa-Boards wie den Heltec V4 oder RAK-WisBlock-Module. Es gibt sie in mehreren Varianten, und die Rolle eines Geräts wird damit fest gesetzt:

  • Ein Companion ist das Funkmodem für dein Handy oder deinen Rechner. Die Oberfläche und der Nachrichtenverlauf leben in der App, die per Bluetooth oder USB verbunden ist. Der Node selbst speichert Kontakte und puffert eingehende Nachrichten, solange das Handy weg ist.
  • Ein Repeater ist Infrastruktur. Er läuft unbeaufsichtigt, idealerweise hoch oben, und leitet Pakete für andere weiter.
  • Ein Room Server ist eine Art kleines Forum auf Funkbasis, dazu weiter unten mehr.

Diese Trennung ist der zentrale Unterschied zu einem Mesh, in dem jedes Gerät mitwiederholt. Bei MeshCore übernehmen das die Repeater. Meine Companions haben den Repeat-Modus aus. Einen eingeschränkten Companion-Repeat gibt es nur für spezielle Frequenzen. Und auch die Repeater wiederholen nicht blind alles: Berechtigte Repeater leiten weiter, abhängig von Scope, Duplikaterkennung, Hop-Limits und Airtime-Budget. Der Repeater-Modus an meinem Companion ist nicht dafür gedacht, wirklich als Repeater im ganzen Netzwerk zu arbeiten, sondern eher, wenn man mit einer kleinen Gruppe unterwegs ist, vielleicht sogar an einer Stelle, an welcher kein anderer Empfang ist und dennoch die Kommunikation sichergestellt sein soll. Klar es gibt den direkten Weg zwischen den einzelnen Geräten aber wenn man mit einer kleinen Gruppe eine längere Wanderung macht, kann es ja passieren, dass die ersten ein gutes Stück weiter vorne sind und die letzten deutlich zurück, dann kann ein Companion mit Repeater-Modus in der Mitte schon helfen, nur ist die Gruppe auf diesen speziellen Frequenzen dann raus aus dem Mesh-Rheinland-Netz.

Flood, Direct und Pfad-Hashes

MeshCore kennt zwei Arten, ein Paket zu verschicken. Beim Flood Routing sendet der Absender das Paket einfach aus, und jeder berechtigte Repeater, der es hört, sendet es nach einer kleinen zufälligen Verzögerung erneut. Die Verzögerung soll verhindern, dass mehrere Repeater gleichzeitig senden. Stellt euch einfach einen Saal voll Leute vor, wenn alle gleichzeitig schreien, hört man auch wieder nichts. Hört ein Repeater dasselbe Paket ein zweites Mal, verwirft er es. Dafür merkt er sich die Hashes der Pakete, die er gesehen hat, nicht die Kennungen der Nodes. Der Hash wird über Pakettyp und Nutzdaten gebildet, ohne Pfad. Die Liste ist ein Ringpuffer mit maximal 160 Einträgen, neue Einträge überschreiben die ältesten. „Dasselbe Paket ein zweites Mal“ erkennt der Repeater also nur, solange es noch in diesen letzten 160 Einträgen steht. Was bisher wohl ganz okaiish klappt, zumindest habe ich noch kein Problem bewusst gesehen.

Gegen Schleifen gibt es einen eigenen Mechanismus, nur für Flood-Pakete. Der Repeater zählt, wie oft sein eigener Pfad-Hash schon im Pfad des Pakets steht, und leitet ab einer bestimmten Anzahl nicht mehr weiter. Wie streng das ist, stellt man mit set loop.detect auf off, minimal, moderate oder strict ein. Mit 2-Byte-Pfad-Hashes und minimal, wie bei mir, ist bei zwei Vorkommen Schluss. Details stehen in der MeshCore CLI-Doku zur Loop Detection.

Dabei wird übrigens die Uhr wichtig. Der Zeitstempel des Absenders steckt in den verschlüsselten Nutzdaten. Weil die Verschlüsselung deterministisch ist, ist er bei Textnachrichten das, was zwei gleiche Texte zu zwei verschiedenen Paketen macht. Bei Text- und Kanalnachrichten aus der App liefert die App diesen Zeitstempel vom Handy mit, da spielt die Uhr des Nodes keine Rolle. Seine eigene Uhr nutzt der Node aber für seine Adverts und für die Anmeldung an Repeatern und Room Servern. Beides ist über den Zeitstempel gegen Wiederholung geschützt: Ein Advert, das älter ist als das zuletzt gesehene, wird ignoriert, und eine Anmeldung per Passwort mit älterem Zeitstempel als die letzte wird abgelehnt. Kommt ein Node nach einem Neustart mit falscher, zurückgestellter Uhr hoch, laufen seine Adverts und Anmeldungen also im Zweifel ins Leere, bis die Uhr wieder stimmt. Noch ein Grund für die RTC, zu der ich in Teil 3 komme.

Jeder Repeater hängt beim Weiterleiten eine kurze Kennung an das Paket, die ersten ein bis drei Bytes seines öffentlichen Schlüssels. MeshCore nennt das Pfad-Hash, es ist aber einfach der Anfang des Schlüssels. Ein Flood-Paket trägt also die Liste der Repeater mit, über die es gelaufen ist, zum Beispiel 84ca. Kommt eine Direktnachricht so beim Empfänger an, kennt er damit den Weg und schickt ihn zusammen mit der Bestätigung an den Absender zurück. Danach kennen beide Seiten einen funktionierenden Pfad.

Ab dann greift Direct Routing: Das Paket bekommt den bekannten Pfad mit, und nur der jeweils nächste Repeater in dieser Liste leitet weiter. Alle anderen bleiben still. Das spart eine Menge Airtime. Bricht der Pfad weg, etwa weil du mit dem Handy-Node unterwegs bist und ein anderer Repeater näher ist, fällt die Nachricht wieder auf Flood zurück, und der Pfad wird neu gelernt. Ich muss zugeben, den Punkt erst spät verstanden zu haben. *hust* bestimmt weil ich da nicht hin geschaut habe (klingt fast, wie eine Ausrede oder?).

Wie groß so ein Pfad-Hash ist, legt der Absender fest, möglich sind 1, 2 oder 3 Byte pro Hop. Bei einem Byte kollidieren die Kennungen in einer so dichten Gegend wie hier ständig, deshalb bittet die lokale Community um 2 Byte. Warum das so ist und wo man es einstellt, steht in Teil 6.

Damit Nodes sich überhaupt kennen, gibt es Adverts. Ein Advert ist ein signiertes Paket mit öffentlichem Schlüssel, Name, Typ und optional einer Position. Repeater senden ihre Adverts in langen Abständen als Flood, hier in der Gegend typischerweise etwa alle 50 Stunden. Companions schicken ihr Advert normalerweise nur, wenn du es in der App auslöst. Die App legt gehörte Nodes dann als Kontakt an. Adverts zu spammen ist also eher nicht sinnvoll. Wenn du an einem neuen Ort bist oder neue weitere Companions in der Nähe sind, mit welchen du deinen „Kontakt“ teilen möchtest, dann hau halt einen Advert raus. Einfach so, doof. Kurz: Adverts am Companion senden, sollte immer einen Grund haben, den du erklären kannst. „Nur mal so“ ist dabei kein Grund.

Ein letztes Konzept nur als Vorgeschmack: Region-Scopes. Ein Flood-Paket kann eine Region wie de oder rheinland tragen, und Repeater entscheiden anhand dieser Markierung, ob sie es weiterleiten. Hier im Rheinland ist das der wichtigste Stellhebel überhaupt, er bekommt in Teil 6 seinen Platz, also öhm, noch warten bis ihr das lesen könnt.

Kontakte, Schlüssel und warum der Name nichts beweist

Jeder MeshCore-Node hat ein Ed25519-Schlüsselpaar. Der öffentliche Schlüssel ist 32 Byte lang und ist die eigentliche Identität. Der Name im Advert ist zwar von der Signatur abgedeckt, aber nicht eindeutig. Jeder kann seinen Node „Kernel-Error“ nennen. Ein Kontakt mit passendem Namen sagt also nur, dass irgendwer diesen Namen gewählt hat.

Wer denkt schon jetzt an Verschlüsselung? Ja, ich auch. Ihr wisst ja, wie gerne ich Verschlüsselung habe und diese gibt es auch hier. Vielleicht mit ein Grund, warum mich das Thema so abholt? Für Direktnachrichten leiten beide Seiten per ECDH aus ihren statischen Schlüsseln ein gemeinsames Geheimnis ab. Die Nutzlast wird mit AES-128 verschlüsselt, blockweise im ECB-Modus, und mit einem auf 2 Byte gekürzten HMAC-SHA256 abgesichert. Das spart Bytes, hat aber Folgen: keine Forward Secrecy, gleiche 16-Byte-Blöcke sehen unter ECB gleich aus, und ein gefälschtes Paket kommt mit einer Wahrscheinlichkeit von 1 zu 65.536 durch den MAC. Pfade und Pakettypen sieht ohnehin jeder, der mithört. Gegen neugierige Zuhörer reicht das. Für Vertrauliches nimm einen Ende-zu-Ende-Messenger.

Denkst du genau jetzt an dein WhatsApp auf dem Smartphone? btw.: MeshRheinland verweist da auf ihre WhatsApp Community, was mich etwas enttäuscht.

Kanäle nutzen dagegen einen gemeinsamen 16-Byte-Schlüssel. Der des Public-Kanals ist überall derselbe, der Kanal ist also öffentlich. Ich habe auch noch ein paar Screenshots für euch, da habe ich öffentliche Informationen/Nachrichten auch nicht weiter versteckt. Weil, na sie eh für jeden öffentlich einsehbar sind 😀

Wenn du sicher sein willst, mit wem du schreibst, vergleichst du den öffentlichen Schlüssel über einen unabhängigen Weg, also persönlich per QR-Code, über einen bereits verifizierten Messenger oder am Telefon. Ein Advert mit dem richtigen Namen ist keine Verifikation. Wie das in der App aussieht, zeige ich in Teil 2, dort steht auch der Kontakt meines Companions.

Room Server

Kanäle sind reine Broadcasts. Wer gerade nicht empfängt, hat die Nachricht verpasst. Ein Room Server löst das wie ein kleines Schwarzes Brett: Mitglieder melden sich an, die letzten 32 Beiträge liegen in einem Ringpuffer auf dem Node und werden jedem angemeldeten Mitglied einzeln und verschlüsselt zugestellt. Wer beim Schreiben offline war, bekommt die verpassten Beiträge nach der nächsten Anmeldung nachgeliefert, aber nur, solange sie noch in diesen 32 Einträgen stehen. Ob ein Raum offen ist, entscheidet sein Betreiber über das Passwort. Mit allow.read.only dürfen auch Gäste ohne Passwort mitlesen. Hier in der Umgebung gibt es mehrere, darunter „Hi @ meshrheinland.de“ von der Community.

Warum MeshCore und nicht Meshtastic

Der Heltec V4 kam mit Meshtastic vorinstalliert, und Meshtastic ist ein ausgereiftes Projekt mit großer Verbreitung und guten Apps. Dort können je nach Rolle auch Endgeräte weiterleiten, MeshCore trennt Endgerät und Infrastruktur strikter, was geil ist. Entscheidend war für mich aber noch etwas anderes. Ein Mesh ist nur so gut wie die Nodes in Funkreichweite, also wollte ich wissen, was hier wirklich läuft.

Dafür habe ich die öffentlichen Node-Listen beider Projekte als JSON gezogen, für Meshtastic von meshmap.net, für MeshCore von map.meshcore.dev. Ein kleines Python-Skript filtert nach Luftlinie um meinen Standort im Raum Rheinbach und nach dem Zeitpunkt, an dem der Server den Node zuletzt gesehen hat. Achtung, falls du das nachbaust: Nimm den serverseitigen Zeitstempel. Das Feld last_advert bei MeshCore stammt von der Uhr des Nodes, und die liegt teils Jahre daneben, manchmal auch in der Zukunft. Die Teile haben meist keine RTC und daher kommen die gerne beim Restart mit „irgendwas“ an Datum/Uhrzeit hoch. Keine Sorge, wie wir eine RTC dazu bekommen und das rebootfest machen, kommt auch noch.

RadiusMeshCore 24 hMeshtastic 24 hMeshCore 14 TageMeshtastic 14 Tage
1 km4050
5 km4060
10 km73113
20 km335625
40 km912019220

Die Zahlen sind kumulativ. Von den 192 MeshCore-Nodes im 40-km-Radius waren 177 Repeater, 9 Room Server und 6 Companions. Der nächste MeshCore-Repeater stand nur wenige hundert Meter entfernt, der nächste Meshtastic-Router einige Kilometer.

Fair bleiben muss man trotzdem. meshmap.net kennt nur Meshtastic-Nodes, die über MQTT-Gateways gemeldet werden, und verwirft Positionen nach etwa 8 Stunden. Die 14-Tage-Spalte ist für Meshtastic deshalb strukturell zu niedrig, die 24-Stunden-Spalte ist der fairere Vergleich. Auch dort steht es bei 40 km noch 91 zu 20. Auf der anderen Seite beweist ein Eintrag auf der MeshCore-Karte keine nutzbare Funkverbindung. Die Einträge kommen von Uploadern und aus der App, Positionen können ungenau oder veraltet sein.

Dazu kommen die Funkparameter. 178 der 192 MeshCore-Einträge meldeten 869,618 MHz, 62,5 kHz, SF8 und CR 4/8, die übrigen 14 CR 4/5. Diese Werte meldet allerdings der empfangende Uploader, sie sind keine authentifizierte Angabe des Senders. An der Richtung ändert das nichts. Hier gibt es eine aktive MeshCore-Community, die Mesh Rheinland, mit klaren Empfehlungen für Preset und Konfiguration und mit CoreScope als Live-Karte und Paketanalyse. Bei Repeatern in ein paar hundert Metern Entfernung geht es für meinen Dach-Node also weniger um erste Abdeckung als darum, einen gut platzierten zusätzlichen Hop beizusteuern.

Kartenansicht im Linux-Client Meshy mit dunklem Kartenhintergrund zwischen Nijmegen, Hamm und Koblenz, Kontakte als Cluster mit Zahlen wie 53 bei Bonn und 51 bei Düsseldorf, links Filter für Users, Repeaters, Rooms und Sensors
Was ein einzelner Companion kennt: 140 von 150 Kontakten mit Position, verteilt von Nijmegen bis Koblenz

Wer lieber zuhört, dem empfehle ich den MRMCD-2026-Vortrag „MeshCore: Grenzenloser Spaß auf 868 MHz“ und den Beitrag „MeshCore: So kommuniziert ihr auch beim Blackout!“ von heise und c’t.

Mein Aufbau und die Serie

Stand Anfang November 2026 sieht mein Setup so aus: Ein Heltec V4 läuft als fester Repeater „DE-NW-SU-Todenfeld-KE“ in einem Aluminiumgehäuse auf dem Dachboden, mit Akku, Echtzeituhr und einer 5,8-dBi-Antenne am Satellitenmast über dem Dach. Strom und Logging kommen vom Raspberry Pi, der dort oben schon als ADS-B-Feeder arbeitet. Mein persönlicher Companion „Kernel-Error“ ist ein Heltec-V4-Komplettkit, das per Bluetooth am Handy hängt. Ein weiterer Heltec mit GPS fährt als „ErrorCar“ im Auto mit und sammelt Messpunkte für MeshMapper. Dazu kommt ein RAK3312 in einer Akkubox als portabler zweiter Repeater mit eigener Identität und ohne veröffentlichte Position. Ich vermute aber, dass der RAK ein Problemchen hat. Eigentlich war er als mein Repeater unter dem Dach geplant. Da ich die gefühlten Problemchen noch nicht echt gemessen habe, dennoch mit dem Projekt weiter kommen wollte, ohne wieder Hardware doppelt zu kaufen, habe ich mit dem Heltec weiter gemacht. Da wird es ggf. noch etwas außerhalb dieser Serie geben. Messen mit dem tinySA und einer 30 dB Dämpfung dazwischen um die Sendeleistung zu messen und so weiter und so weiter. Was ich ebenfalls noch im Sinn habe, ist ein Sensor, den jeder abfragen kann. Vielleicht für Luftqualität, radioaktive Strahlung, Temperatur usw. Aber das ist noch alles nicht gemacht und hängt davon ab, wie viel Zeit und Energie ich aufbringen kann; das ist im Moment Mangelware bei mir.

Die Serie geht diesen Weg der Reihe nach durch:

Bevor du Hardware kaufst

Schau in die Karte deiner Community, bevor du dich für ein Projekt entscheidest. Auf map.meshcore.dev und meshmap.net siehst du in ein paar Minuten, was in deiner Gegend wirklich aktiv ist, und wenn es eine lokale Gruppe gibt, verrät die dir auch das passende Preset. Überleg dir außerdem, ob dein Gerät an der Steckdose, am Akku oder an einer Solarzelle hängen soll. Davon hängt ab, welches Board passt, mehr dazu in Teil 2 und Teil 3. Wenn MeshCore bei dir lebt, geht es in Teil 2 mit dem ersten Companion weiter.

Siehe auch: Teil 2: Companion, Teil 3: Repeater, Teil 4: Antennen und Messen, Teil 5: MeshMapper, Teil 6: Konfiguration.

Links zu Amazon und AliExpress sind Affiliate-Links: Kaufst du darüber, bekomme ich eine kleine Provision, für dich ändert sich am Preis nichts. Als Amazon-Partner verdiene ich an qualifizierten Verkäufen. Wenn du also so einen Beitrag hier hilfreich findest, ist das ein Weg um das zu zeigen.

Wenn ihr Fragen habt oder wissen wollt, wie es bei euch in der Gegend mit MeshCore aussieht, dann dürft ihr mich gerne fragen.

BIOS erklärt, Teil 12: elf leere SATA-Anschlüsse und eine Verschlüsselung die es nicht gibt

Diese Folge fängt mit einem Anti-Klimax an: die beiden Storage-Seiten dieses Boards zeigen bei mir ausschliesslich leere Anschlüsse.

SATA Port 0..7                    [Not Installed]
sSATA Port 0..2                   [Not Installed]

Elf Anschlüsse, elf mal nichts. Meine beiden Laufwerke sind NVMe und hängen an PCIe, dazu gab es Teil 8. Trotzdem ist das eine der Seiten, an der man am meisten über die Geschichte von Massenspeicher lernt, und am Ende dieser Folge steht ein Thema, das mich beim Nachlesen mehr beschäftigt hat als erwartet.

Teil 12, alle bisherigen Folgen unter BIOS & Firmware.

Warum es zwei SATA-Seiten gibt

Aptio Setup, PCH SATA Configuration, AHCI-Modus und acht Anschlüsse, alle als Not Installed gemeldet

PCH SATA Configuration mit acht Anschlüssen und PCH sSATA Configuration mit drei. Das sind zwei getrennte Controller im Chipsatz, und das kleine s steht für secondary. Der Grund ist banal: irgendwann reichten die Anschlüsse des Hauptcontrollers nicht mehr, also hat Intel einen zweiten danebengesetzt. Für das Betriebssystem sind es zwei Geräte, und genau so sieht man sie:

lspci -nn | grep -i sata
#   00:11.5 SATA controller: ... C620 Series Chipset Family SSATA Controller [AHCI mode]
#   00:17.0 SATA controller: ... C620 Series Chipset Family SATA Controller [AHCI mode]

Elf Anschlüsse insgesamt, und Linux legt für jeden brav einen Port an, ob da etwas hängt oder nicht:

ls /sys/class/ata_port/
#   ata1 ata2 ata3 ata4 ata5 ata6 ata7 ata8 ata9 ata10

AHCI, und was die Alternative wäre

Configure SATA as                 [AHCI]

Dieser Schalter hat historisch mehr kaputte Windows-Installationen verursacht als vermutlich jeder andere im BIOS. Die Alternativen sind je nach Board IDE und RAID.

IDE lässt den Controller so tun, als wäre er ein Festplattencontroller von 1994. Kein Native Command Queuing, kein Hot Plug, dafür funktioniert es mit wirklich jedem Betriebssystem. AHCI ist der moderne Modus mit allem was dazugehört. RAID schiebt einen firmwaregestützten RAID-Verbund dazwischen, der in Wahrheit vom Treiber gemacht wird.

Der Klassiker war und ist: System im einen Modus installiert, später im BIOS umgestellt, Bluescreen beim nächsten Start, weil der passende Treiber nicht geladen wird. Unter Linux ist das entspannter, aber auch dort ändern sich die Gerätenamen.

AHCI ist auf diesem Board die richtige Wahl, und wäre es auch dann, wenn Laufwerke steckten. Softwareseitiges RAID ist unter Linux besser aufgehoben als in einer Chipsatz-Firmware, deren Metadatenformat man später nirgends mehr lesen kann.

Hot Plug, pro Anschluss

SATA Port 0
  Hot Plug                        [Enable]
  Spin Up Device                  [Disabled]
  SATA Device Type                [Hard Disk Drive]

Jeder der elf Anschlüsse hat sein eigenes kleines Untermenü mit denselben drei Einstellungen. Hot Plug erlaubt das Wechseln im laufenden Betrieb. Auf einem Serverboard ist das selbstverständlich an, weil dort Wechselrahmen sitzen. In einem Tower ohne Wechselrahmen ist es eine theoretische Freiheit, aber es kostet auch nichts.

Spin Up Device ist ein Relikt aus der Zeit rotierender Platten. Eingeschaltet lässt die Firmware die Laufwerke gestaffelt anlaufen statt alle gleichzeitig. Der Anlaufstrom einer Festplatte ist ein Vielfaches ihres Betriebsstroms, und wer zwölf davon gleichzeitig hochfährt, kann ein knapp bemessenes Netzteil in die Knie zwingen. Bei SSDs ist das gegenstandslos, bei einem Board für zwölf Platten nicht.

SATA Device Type unterscheidet zwischen Festplatte und SSD und dient vor allem dazu, der Firmware zu sagen ob sie mit Anlaufzeiten rechnen muss.

Support Aggressive Link Power Mgmt [Disable] ist das SATA-Gegenstück zu ASPM aus Teil 10: Stromsparzustände für die Verbindung. Auf Servern traditionell aus, weil es in der Vergangenheit zu sporadischen Abbrüchen geführt hat. Auf Notebooks dagegen unverzichtbar für die Akkulaufzeit.

Und jetzt zum interessanten Teil

Aptio Setup, HDD Security, zwei Laufwerke mit geschwärzten Seriennummern und dem Vermerk Not TCG oder SAT3 Supported

Unter Security > HDD Security Configuration steht das hier:

HDD Name                          MZXLR3T2HBLS-000H3
HDD Serial Number                 (geschwärzt)
Security Mode                     Not TCG/SAT3 Supported
Security Function                 [Disable]

Zweimal dasselbe, weil zwei gleiche Laufwerke stecken. Und die dritte Zeile ist der Grund für diesen Abschnitt.

Es gibt seit langem eine Funktion, die im Laufwerk selbst sitzt und wenig bekannt ist: ATA Security. Man setzt ein Passwort direkt auf die Platte, und ohne dieses Passwort gibt sie keine Daten heraus. Kein Treiber, keine Software, das Laufwerk selbst verweigert die Arbeit.

Bei vielen modernen SSDs kommt etwas dazu, das die Sache erst richtig interessant macht: sie verschlüsseln ihre Daten intern ohnehin, auch ohne dass jemand ein Passwort gesetzt hat. Der Controller erzeugt beim ersten Start einen zufälligen Schlüssel, verschlüsselt damit alles was in den Flash geht, und entschlüsselt beim Lesen wieder. Das Betriebssystem merkt davon nichts.

Der Zweck ist dabei zunächst gar nicht Sicherheit, und das fand ich beim Nachlesen die überraschendste Erkenntnis: es geht um das Löschen. Wer ein Passwort setzt, schützt damit diesen bereits vorhandenen Schlüssel.

Denn daraus folgt der Trick, den ich vor einer Weile selbst genutzt habe. Ein Secure Erase auf so einem Laufwerk überschreibt nichts. Er wirft den internen Schlüssel weg und würfelt einen neuen. Danach sind sämtliche Daten auf dem Flash zwar noch physisch da, aber nur noch als Zufallsrauschen. Das ist nicht nur schneller als Überschreiben, es schont auch die Zellen, denn jedes Überschreiben kostet Lebensdauer. Das Ganze dauert entsprechend nicht Stunden, sondern:

hdparm --user-master u --security-set-pass p /dev/sda
hdparm --user-master u --security-erase-enhanced p /dev/sda

Bei den Samsung-SSDs, die ich so gelöscht habe, war das in unter einer Sekunde erledigt, obwohl das Werkzeug acht Minuten angekündigt hatte. Eine Intel-SSD im selben Durchgang brauchte siebzehn Sekunden. Beide waren danach leer, aber unterschiedlich leer: die Intel lieferte durchgehend Nullen, die Samsungs eine Mischung aus Nullen und Rauschen. Beides ist korrekt, es sind nur zwei verschiedene Herangehensweisen. Wer Nullen als Erfolgskriterium erwartet, hält ein Samsung-Ergebnis fälschlich für einen Fehlschlag.

Meine Laufwerke können das nicht

Not TCG/SAT3 Supported heisst genau das: diese beiden PM1735 unterstützen weder TCG Opal noch den ATA-Security-Durchgriff über SAT3. Es gibt hier also kein Laufwerkspasswort und keinen selbstverschlüsselnden Betriebsmodus, den man aktivieren könnte.

Das ist für Enterprise-NVMe nicht ungewöhnlich. Die Modellpalette wird üblicherweise in Varianten mit und ohne Verschlüsselungsfunktion aufgeteilt, weil manche Kunden aus regulatorischen Gründen ausdrücklich Laufwerke ohne Krypto brauchen. Meine sind offenbar aus der zweiten Gruppe.

Praktisch macht mir das nichts aus, weil ich ohnehin eine Ebene höher verschlüssele. Die Systemplatte läuft über LUKS, darüber liegt LVM, darin Wurzeldateisystem und Auslagerung. Damit ist alles verschlüsselt, inklusive Swap, und zwar mit einem Schlüssel den ich kontrolliere und nicht mit einem, der im Controller eines Laufwerks liegt.

Der Unterschied ist nicht nur philosophisch. Bei Laufwerksverschlüsselung vertraut man einer proprietären Implementierung, die man nicht prüfen kann, und es hat in der Vergangenheit mehrfach Untersuchungen gegeben, bei denen genau diese Implementierungen erhebliche Schwächen hatten. Bei LUKS kann man nachlesen was passiert.

Der Preis ist Rechenzeit, und der ist dank AES-NI vernachlässigbar. Dazu gab es in Teil 4 einen längeren Abschnitt.

Was ich stattdessen im Blick behalte

Wenn die Laufwerke selbst keine Sicherheitsfunktion anbieten, bleibt als Aufgabe des BIOS eigentlich nur, sie sauber anzubinden. Alles Weitere spielt sich im Betriebssystem ab, und dort sieht es gut aus:

smartctl -H /dev/nvme0
#   SMART overall-health self-assessment test result: PASSED

Available Spare:                    100%
Percentage Used:                    0%
Media and Data Integrity Errors:    0
Error Information Log Entries:      0
Power On Hours:                     3.166

Null Prozent Abnutzung nach gut dreitausend Betriebsstunden, keine Medienfehler, kein einziger Eintrag im Fehlerprotokoll. Eine Zeile fällt mir trotzdem auf: dreissig unsaubere Abschaltvorgänge. Das ist für eine Maschine, die auch mal hart ausgeschaltet wird, im Rahmen, aber es ist die Zahl, bei der ich hinsehe wenn sie plötzlich schneller steigt.

Fazit

Elf leere Anschlüsse und ein Laufwerk ohne Verschlüsselungsfunktion klingt nach einer mageren Folge. Ich fand sie trotzdem lohnend, weil zwei Dinge dabei herauskamen. Erstens die Erkenntnis, warum ein Secure Erase auf einer SSD in einer Sekunde fertig ist und trotzdem gründlicher als jedes Überschreiben. Und zweitens die Bestätigung, dass die Verschlüsselungsentscheidung an dieser Maschine bereits richtig getroffen ist, weil sie gar nicht anders getroffen werden konnte.

Nächste Folge: das Vorstart-Netzwerk. PXE, iSCSI, Option ROMs von Netzwerkkarten, und eine Karte die dem BIOS meldet, dass ihr eigener Treiber zu alt für sie selbst ist.

Siehe auch

Falls jemand von euch ATA Security produktiv einsetzt statt einer Softwarelösung: warum? Das würde mich ehrlich interessieren, ihr dürft mich jederzeit fragen.

pwn.watch: Sicherheitsvorfälle im Überblick, mit Vorsicht beim Brief für die AI

Terminalfenster mit Feeds für php, wordpress und nginx und dem Vorfall php2shell, daneben der Schriftzug pwn.watch, Sicherheitsvorfälle im Überblick, darunter: Empfehlung, mit Vorsicht beim AI-Brief

Bei kritischen Lücken wie gerade bei php2shell ist die erste Meldung selten das Problem. Schwieriger sind die Tage danach: Welche Versionen sind wirklich repariert, wird die Lücke schon ausgenutzt, welcher Hersteller hat für seine Appliances schon Updates, und welche Meldung war doch nur ein Gerücht? Michael hat mir dafür pwn.watch empfohlen, und ich finde die Seite nützlich genug, um die Empfehlung hier weiterzugeben. Ein paar Punkte solltest du dabei aber im Hinterkopf behalten.

Was pwn.watch ist

pwn.watch sammelt aktuelle Sicherheitsvorfälle und teilt sie in drei Bereiche: Lücken in Software, die du selbst betreibst und patchen musst, Angriffe auf Firmen und Dienste, bei denen du höchstens Zugangsdaten tauschen kannst, und Supply-Chain-Vorfälle wie manipulierte Pakete. Hinter der Seite steht der Entwickler Kitze, finanziert wird sie über Sponsoren in der Seitenleiste.

Jeder Vorfall bekommt eine eigene Seite mit Patch-Status, betroffenen und reparierten Versionen, einer Zeitleiste und den Quellen dazu. Gerade die Zeitleiste macht den Unterschied zu einer einzelnen Nachrichtenmeldung. Beim Eintrag zu php2shell stehen zum Beispiel die ersten Ausnutzungsversuche, die laut AWS schon vor dem Release begannen, die Einschätzung von Sansec, dass die öffentlichen Exploits meist eine zusätzliche Lücke in der Anwendung brauchen, eine Regression in den neuen Versionen 8.4.27 und 8.5.12 und die Advisories der Hersteller von Appliances. Unbestätigte Behauptungen sind als solche markiert. Für so etwas hätte ich sonst ein Dutzend Tabs offen.

Nur das, was du auch betreibst

Am praktischsten finde ich die Feeds. Es gibt RSS und JSON nicht nur für alles, sondern auch pro Tag, also pro Hersteller, Produkt oder Plattform. Wer wie ich PHP, WordPress und nginx betreibt, abonniert einfach diese Feeds und bekommt nur noch die Vorfälle, die ihn auch betreffen:

https://pwn.watch/t/php/feed.xml
https://pwn.watch/t/wordpress/feed.xml
https://pwn.watch/t/nginx/feed.xml

Die Übersicht aller Tags findest du unter pwn.watch/tags. Für meinen Stack fehlen allerdings einige Dinge: Für FreeBSD, OpenSSL, Postfix oder Dovecot gibt es keinen eigenen Tag. Die Seite ersetzt deshalb weder die FreeBSD Security Advisories noch ein regelmäßiges pkg audit -F, sie ergänzt beides.

Der Button „Copy brief for your AI“

Auffällig ist der Button „Copy brief for your AI“ auf jeder Seite. Er erzeugt im Browser einen Markdown-Text und legt ihn in die Zwischenablage: was betroffen ist, welche Versionen repariert sind, ein paar Befehle zum Prüfen wie php -v oder nginx -v, Hinweise zum Patchen und die Links zu den Advisories. Den Text soll man direkt in einen Coding-Agenten wie Claude Code oder Cursor kopieren, der dann prüft, ob man betroffen ist, und im besten Fall gleich aktualisiert. Dazu gibt es eine llms.txt und die Daten jedes Vorfalls als JSON. Die Seite ist also ausdrücklich darauf ausgelegt, dass Agenten sie lesen.

Hier wäre ich vorsichtig. Die Prüfbefehle stammen aus einer festen Liste im Code der Seite und sind harmlos. Der Rest des Textes ist aber Freitext, den allein der Betreiber kontrolliert und der zum Teil aus Tweets und Artikeln zusammengetragen wird. Wenn du so etwas ungelesen an einen Agenten gibst, der Shell-Zugriff hat, vertraust du darauf, dass dieser Text nie manipuliert wird. Das ist genau der Weg, über den Prompt Injection funktioniert. Dazu kommt, dass der Brief nur den einfachen Teil abdeckt. Bei php2shell steht darin „upgrade to 8.3.36“. Die eigentliche Arbeit lag bei mir aber im Bauen aus den Ports, den Extensions und dem plötzlich falschen CA-Bundle. Das hätte kein Agent aus diesem Text gewusst. Nutz am besten die Vorschau neben dem Button und lies den Brief als Zusammenfassung, nicht als Arbeitsauftrag.

Mein Fazit

pwn.watch ist ein Ein-Personen-Projekt ohne Angaben dazu, wie die Daten gepflegt werden, und schreibt selbst, dass es keine Sicherheitsberatung ist und man vor dem Handeln das Advisory des Herstellers prüfen soll. So würde ich die Seite auch nutzen: als schnellen Überblick, als Sammlung der relevanten Quellen und als Feed für den eigenen Stack. Die Entscheidung, was auf dem eigenen Server passiert, nimmt sie dir nicht ab, und das sollte auch kein Agent für dich tun, der nur diesen Text kennt.

BIOS erklärt, Teil 11: VT-d, 102 IOMMU-Gruppen und ein Menü das falsch beschriftet ist

Wenn man einer virtuellen Maschine eine echte Grafikkarte geben will, stösst man ziemlich schnell auf einen Begriff, der wie eine Ausrede klingt: IOMMU-Gruppen. Man liest dann Sätze wie „deine Karte ist in derselben Gruppe wie der USB-Controller, also geht das nicht“, und wenn man Pech hat, findet man kurz darauf Anleitungen mit dem Wort „ACS-Override“ darin, die einem erklären wie man diese Regel umgeht und dabei die Isolation aushebelt.

Der Ursprung dieser ganzen Geschichte liegt in einem einzigen BIOS-Untermenü, und um das geht es heute. Teil 11 der Serie, alle Folgen unter BIOS & Firmware. Wir sind unter Advanced > Chipset Configuration > North Bridge > IIO Configuration, und diesmal in den Untermenüs statt auf der Übersicht.

VT-d, oder: wer darf in den Speicher schreiben

Aptio Setup, Intel VT for Directed I O, mit ACS Control, Pre-boot DMA Protection und Interrupt Remapping
Intel(R) VT for Directed I/O (VT-d)   [Enable]
ACS Control                           [Enable]
Pre-boot DMA Protection               [Disable]
Interrupt Remapping                   [Auto]

Das Grundproblem, das VT-d löst, ist älter als Virtualisierung. Eine PCIe-Karte darf per DMA selbständig in den Arbeitsspeicher schreiben, ohne die CPU zu fragen. Das ist der Grund, warum Netzwerkkarten und Laufwerke überhaupt schnell sind. Es bedeutet aber auch, dass eine Karte in jeden beliebigen Speicherbereich schreiben kann, sofern niemand sie daran hindert.

Die IOMMU ist dieser Jemand. Sie sitzt zwischen den Geräten und dem Speicher und übersetzt Adressen, genau wie die MMU es für Prozesse tut. Ein Gerät sieht dann nur noch den Speicher, der ihm zugewiesen wurde. Damit kann man einer virtuellen Maschine eine echte Karte geben, ohne dass diese Karte den Speicher des Wirtssystems lesen könnte.

Bei mir ist das aktiv, was man am Startprotokoll sieht:

dmesg | grep DMAR-IR
#   DMAR-IR: Enabled IRQ remapping in x2apic mode

Interrupt Remapping ist der zweite Teil derselben Absicherung. Interrupts sind auf modernen Systemen einfach Schreibzugriffe auf eine bestimmte Adresse. Ohne Umsetzung könnte ein Gerät damit Interrupts auslösen, die für andere gedacht sind. Die Umsetzung schiebt eine Tabelle dazwischen.

Nebenbei erklärt das eine Kuriosität aus Teil 4: dort stand ein Schalter namens Extended APIC auf Disable, und trotzdem läuft mein System im x2APIC-Modus. Der Grund steht in der Zeile oben. Der Kernel schaltet x2APIC selbst ein, sobald Interrupt Remapping läuft, weil er es dafür braucht. Die BIOS-Option entscheidet nur, ob die Firmware es vorab tut.

ACS, und die Sache mit den Gruppen

ACS Control [Enable] ist der Schalter, wegen dem ich diese Folge angefangen habe.

Access Control Services regeln, ob zwei Geräte hinter demselben Switch direkt miteinander reden dürfen, ohne dass der Verkehr über die IOMMU läuft. Ohne ACS kann die IOMMU nicht garantieren, dass zwei Geräte voneinander getrennt sind, und muss sie deshalb als eine Einheit behandeln. Das ist eine IOMMU-Gruppe: die kleinste Menge Geräte, die man nur gemeinsam durchreichen kann.

Bei mir sieht das ordentlich aus:

ls /sys/kernel/iommu_groups | wc -l
#   102

102 Gruppen, und die meisten enthalten genau ein Gerät. Das ist der Zustand, den man haben will, und man bekommt ihn nicht geschenkt. Auf typischen Consumer-Boards liegen regelmässig ein halbes Dutzend Geräte in einer Gruppe, weil der Chipsatz sie hinter einem Switch ohne ACS bündelt. Dann kommen die Anleitungen mit dem ACS-Override ins Spiel, und die tun genau das wonach sie klingen: sie lügen dem Kernel vor, es gäbe eine Trennung. Für ein Heimlabor mag das reichen. Es ist trotzdem das Abschalten einer Sicherheitsfunktion, und man sollte wissen dass man es tut.

Ein Beispiel aus meiner Maschine, das den Unterschied zeigt: als vorübergehend eine Beschleunigerkarte in Steckplatz 7 steckte, landete sie allein in Gruppe 6. Alleine in einer Gruppe heisst durchreichbar, ohne Tricks, ohne Override. Das war die Voraussetzung dafür, überhaupt darüber nachzudenken, sie einer virtuellen Maschine zu geben.

Pre-boot DMA Protection [Disable] schützt das Zeitfenster vor dem Betriebssystemstart. Der klassische Angriff dagegen läuft über Thunderbolt: Gerät anstecken während der Rechner im Setup steht, per DMA Speicher auslesen. Dieses Board hat kein Thunderbolt, und wer physisch am Gehäuse steht, hat ohnehin bessere Möglichkeiten. Aus ist hier vertretbar.

VMD, und warum es aus bleibt

Aptio Setup, Intel VMD Technology, NVMe Mode Switch steht auf Auto
Intel(R) VMD Technology
NVMe Mode Switch                  [Auto]
> Intel(R) VMD for Volume Management Device on CPU

Volume Management Device ist eine Erfindung, die vor allem für vorkonfigurierte Server gedacht ist. Eingeschaltet fasst der Prozessor mehrere NVMe-Laufwerke zusammen und präsentiert sie nach aussen als ein einziges Gerät. Das ermöglicht Hot-Plug ohne Betriebssystemunterstützung und ist die Voraussetzung für Intels RAID-Lösung VROC.

Bei mir steht das auf Auto, und der Hilfetext im Setup sagt dazu wörtlich default Auto mode, enable VMD when VROC key presence. Also nur aktiv, wenn ein VROC-Lizenzschlüssel steckt. Es steckt keiner. Die Gegenprobe:

lspci -nn | grep -i "volume management"
#   (keine Ausgabe)

Nichts da, beide Laufwerke hängen direkt an ihren Root Ports.

Und dabei soll es bleiben. Mit eingeschaltetem VMD verschwinden meine NVMe-Laufwerke hinter einem Controller, brauchen einen zusätzlichen Treiber, und sind nicht mehr einzeln als PCIe-Geräte sichtbar. Damit wäre unter anderem das weg, was ich in Teil 10 gerade mühsam eingerichtet habe: die AER-Zähler pro Gerät. Ich tausche keine funktionierende Diagnose gegen ein RAID, das ich nicht will.

Der dritte Wert wäre Manual, und laut Handbuch erscheint erst dann das Untermenü, in dem sich VMD pro Stack einzeln einschalten lässt. Solange Auto steht, ist dort nichts zu holen, was erklärt warum ich es beim Durchklicken nie geöffnet habe.

Kleine Korrektur in eigener Sache: in meinen Notizen stand lange, VMD stünde auf Disabled. Der Schalter heisst tatsächlich NVMe Mode Switch und kennt Manual, VMD und Auto. Die Wirkung ist dieselbe, die Beschriftung war meine.

IOAT, das Menü das nicht hält was der Name verspricht

Aptio Setup, IOAT Configuration, mit Disable TPH, Prioritize TPH und Relaxed Ordering
IOAT Configuration
Disable TPH                       [No]
Prioritize TPH                    [Disable]
Relaxed Ordering                  [No]

Ich habe dieses Menü mit einer klaren Erwartung geöffnet. IOAT steht für I/O Acceleration Technology, dahinter stecken DMA-Engines im Prozessor, die Speicherkopien erledigen ohne die Kerne zu beschäftigen. Ich hatte mit Schaltern wie Enable IOAT und DCA gerechnet. Beide gibt es nicht.

Stattdessen stehen dort drei Optionen, die mit dem Namen des Menüs wenig zu tun haben. TPH ist TLP Processing Hint: ein PCIe-Gerät kann einem Schreibzugriff einen Hinweis mitgeben, wohin die Daten am besten sollen, etwa direkt in den Cache eines bestimmten Kerns statt in den Hauptspeicher. Für eine Netzwerkkarte, deren Pakete gleich darauf verarbeitet werden, spart das den Umweg.

Relaxed Ordering erlaubt es, Transaktionen in anderer Reihenfolge abzuarbeiten als sie eingetroffen sind. Das kann Durchsatz bringen und setzt voraus, dass die Beteiligten keine Annahmen über die Reihenfolge treffen. Standard ist deshalb No.

Die DMA-Engines selbst laufen übrigens, ganz ohne sichtbaren Schalter:

ls /sys/class/dma/
#   dma0chan0  dma1chan0  ...  dma7chan0

Acht Kanäle. Der Name des Menüs stimmt also, der Inhalt passt nur nicht dazu.

Das Ganze hat mir nebenbei einen Fehler in meiner eigenen Dokumentation gezeigt. In meiner ersten Aufzeichnung war ein Bildschirm mit genau diesen drei Zeilen, und ich hatte ihn als „Slot-Unterseite“ abgelegt, weil ich das Menü nicht geöffnet hatte und nur den Inhalt sah. Es war die IOAT-Seite. Ich hatte sie die ganze Zeit, nur unter dem falschen Namen.

Was ich davon tatsächlich nutze

Ehrlich? Von diesen vier Menüs nutze ich aktiv genau eines. VT-d ist an und tut seinen Dienst, die Isolation ist sauber, 102 Gruppen sprechen für sich. VMD ist aus und soll aus bleiben, IOAT läuft von selbst, und die TPH-Einstellungen fasse ich nicht an ohne eine Messung zu haben.

Trotzdem ist es eine der Seiten, die ich nach diesem Durchgang anders sehe. Vorher war „IOMMU“ für mich ein Wort aus Passthrough-Anleitungen. Jetzt weiss ich, welcher Schalter dafür verantwortlich ist, dass meine Gruppen sauber getrennt sind, und warum das auf anderen Boards oft nicht so ist.

Nächste Folge: Storage. AHCI, Hot Plug, elf SATA-Anschlüsse von denen keiner belegt ist, und eine Sicherheitsfunktion in Laufwerken, die meine beiden NVMe nicht können.

Siehe auch

Falls jemand von euch ACS-Override im Einsatz hat: aus Not oder bewusst? Und hat es je Ärger gemacht? Ihr dürft mich gerne fragen.

php2shell: PHP am Tag der Veröffentlichung aus den FreeBSD-Ports gepatcht

Warndreieck neben dem Schriftzug php2shell mit CVE-2026-109618 und CVE-2026-109619, reparierte Versionen 8.5.12, 8.4.27, 8.3.36 und 8.2.35, darunter: gepatcht, Blog wieder online

Wie im letzten Beitrag angekündigt, war mein Blog offline, bis das PHP-Update für die beiden kritischen Lücken auf dem Server eingespielt ist. Das ist inzwischen passiert, der Blog ist wieder erreichbar. Seit dem Nachmittag des 21. Oktober sind auch die technischen Details öffentlich, heise security hat sie im Artikel php2shell: Kritische Sicherheitslücken erlauben Codeeinschleusung in PHP zusammengefasst. In diesem Beitrag geht es weniger um die Lücken selbst als darum, wie ich PHP am Tag der Veröffentlichung auf FreeBSD gepatcht habe, über welche Fallen ich dabei gestolpert bin und wie ich geprüft habe, ob vorher schon jemand drin war.

Was hinter php2shell steckt

Unter dem Namen php2shell laufen zwei Fehler im Kern von PHP, beide mit CVSS 9.2 bewertet. CVE-2026-109618 steckt in der Speicherverwaltung. Läuft ein Skript in das eingestellte memory_limit, bricht PHP es ab und räumt danach auf. Mit einem passend gebauten Upload oder tief verschachtelten Arrays gerät genau dieses Aufräumen durcheinander, und am Ende kann ein Angreifer eigenen Code mit den Rechten des Webservers ausführen. CVE-2026-109619 sitzt in der Fehlerbehandlung. Hier bringt eine erste Anfrage PHP in einen kaputten Zustand, den eine zweite Anfrage an denselben Worker ausnutzt.

Die Anwendung selbst muss dafür keinen Fehler haben, und eine Anmeldung braucht der Angreifer auch nicht. Ein Angriff klappt zwar nicht beim ersten Versuch, nach einigen tausend bis zehntausend Anfragen aber zuverlässig. Für automatisierte Massenscans ist das kein Hindernis. Laut Wiz ist jede PHP-Version der letzten zehn Jahre von mindestens einer der beiden Lücken betroffen, das PHP-Team spricht sogar von allen Versionen seit PHP 5. Die reparierten Versionen sind 8.5.12, 8.4.27, 8.3.36 und 8.2.35.

Mein WordPress läuft mit PHP 8.3 als PHP-FPM hinter nginx, zum Zeitpunkt der Meldung in Version 8.3.33. pkg audit -F hat das Paket direkt für beide CVEs als verwundbar gemeldet. Laut Advisory ist Apache mit mod_php nachweislich per HTTP angreifbar, FPM gilt nach Analyse des Quellcodes ebenfalls als anfällig, auch wenn dafür noch kein vollständiger Exploit gezeigt wurde. Darauf wollte ich nicht wetten. Also habe ich nginx gestoppt, noch bevor es einen Patch gab. PHP-FPM lauscht bei mir nur auf einem Unix-Socket und ist ohne nginx davor von außen nicht erreichbar. Damit war die Angriffsfläche weg, ohne dass ich an PHP selbst etwas ändern musste.

Ports statt Paket

Der erste Reflex auf FreeBSD ist pkg upgrade. Nur lag im Paket-Repository auch nach pkg update -f noch PHP 8.3.35, und das ist verwundbar. Im Ports-Tree sah es anders aus: Der Commit lang/php83: Update version 8.3.35=>8.3.36 war wenige Minuten nach dem Upstream-Release in main und kurz darauf auch im Quartalszweig 2026Q4. Bis daraus ein Binärpaket im Repository wird, vergeht deutlich mehr Zeit. Wer einen Port sauber bauen kann, muss bei solchen Lücken also nicht warten.

Ist das wirklich der richtige Fix?

Dass ein Port eine neue Versionsnummer hat, heißt noch nicht, dass darin der richtige Fix steckt. Bevor ich gebaut habe, habe ich deshalb zwei Dinge geprüft. Erstens die Prüfsumme: Die SHA256-Summe von php-8.3.36.tar.xz aus der distinfo des Ports muss mit der Angabe auf php.net übereinstimmen.

cat /usr/ports/lang/php83/distinfo
curl -sS 'https://www.php.net/releases/index.php?json&version=8.3.36'

Beide Quellen nennen dd54693aa334c5c4d72e9aee96b082c2473750a18a6c1d105789c93a3b1692fd. Zweitens den Changelog: Der offizielle Changelog zu 8.3.36 enthält genau einen Eintrag, „Core: Fixed GHSA-w8hw-w9fx-j79w, GHSA-6wmf-4rv5-p3rp (Avoid state corruption via bailout)“. GHSA-w8hw-w9fx-j79w ist genau das Advisory, auf das auch heise verweist, und „Bailout“ ist der Abbruchpfad, den PHP bei einem Fatal Error nimmt. Das passt zu beiden Lücken. Die dritte Bestätigung kam nach der Installation: pkg audit -F meldet für PHP nichts mehr, die VuXML-Datenbank von FreeBSD stuft 8.3.36 also ebenfalls als repariert ein.

Bauen und tauschen

Vor dem Umbau habe ich einen ZFS-Snapshot der Jail angelegt, damit ich bei Problemen in Sekunden zurück kann. Danach ging es grob so weiter, hier vereinfacht ohne den jexec-Präfix:

# Rollback-Punkt
zfs snapshot zroot/jails/<jail>@pre-php2shell-$(date +%Y%m%d-%H%M)

# Ports-Tree auf denselben Zweig wie das Paket-Repository
cd /usr/ports && git fetch origin && git checkout 2026Q4 && git pull --ff-only

# Optionen gegen das installierte Paket abgleichen
pkg query '%Ok=%Ov' php83
make -C /usr/ports/lang/php83 showconfig

# Bauen, dann tauschen
make -C /usr/ports/lang/php83 -DBATCH clean build
make -C /usr/ports/lang/php83 -DBATCH deinstall install

Wichtig waren dabei die Extensions. Auf FreeBSD sind die PHP-Kern-Extensions eigene Pakete wie php83-mysqli, php83-curl oder php83-opcache. Technisch hätten die alten Extensions auch mit dem neuen Kern funktioniert, weil sich die Modul-API innerhalb von 8.3 nicht ändert. Der Sprung von 8.3.33 auf 8.3.36 enthält aber auch alle Korrekturen aus 8.3.34 und 8.3.35, und die stecken zu einem großen Teil in Extensions wie mysqlnd, openssl, phar, soap und filter. Nur den Kern zu tauschen hätte diese Lücken offen gelassen. Ich habe deshalb alle 43 Kern-Extensions aus demselben Tree neu gebaut. PECL-Extensions wie redis, APCu oder imagick haben eigene Versionsnummern und blieben, wie sie waren.

for o in $(pkg query -g '%o %v' 'php83-*' | awk '$2 ~ /^8\.3\.33/ {print $1}' | grep -v '^lang/php83$'); do
    make -C /usr/ports/$o -DBATCH clean build && make -C /usr/ports/$o -DBATCH deinstall install clean
done

Zum Schluss der Punkt, den man leicht vergisst: Ein neues Binary auf der Platte heißt nicht, dass der laufende Dienst es auch benutzt. PHP-FPM hatte noch das alte Programm im Speicher. Also neu starten und nachsehen, welches Binary der Prozess tatsächlich ausführt:

service php_fpm restart
procstat -b $(cat /var/run/php-fpm.pid)

Drei Stolperfallen

Ganz glatt lief es nicht. Die drei Probleme, die mir begegnet sind, dürften auch auf anderen Systemen auftauchen.

Build-Abhängigkeiten aus den Quellen. Der erste Versuch wollte cmake-core aus den Quellen bauen, eine reine Build-Abhängigkeit von re2c, und brach dabei ab. Solche Abhängigkeiten laufen nur während des Bauens und müssen nicht gepatcht sein. Mit make missing sieht man, was fehlt, und kann es einfach als Binärpaket per pkg install nachziehen.

deinstall install entfernt das PHP, das der Port selbst braucht. Beim Erzeugen der Paketliste ruft der Port ein php aus dem PATH auf. Nach dem deinstall gab es keins mehr, die Installation brach mit php: not found ab, und PHP war für ein paar Minuten gar nicht installiert. Da nginx ohnehin aus war, hatte das keine Außenwirkung. Gelöst habe ich es, indem ich das frisch gebaute CLI-Binary für diesen einen Aufruf vorne in den PATH gestellt habe:

PATH=/usr/ports/lang/php83/work/php-8.3.36/sapi/cli:$PATH make -C /usr/ports/lang/php83 -DBATCH install

Beim nächsten Mal lasse ich zuerst make stage laufen, solange das alte PHP noch installiert ist, und tausche erst danach.

Andere OpenSSL-Basis, anderes CA-Bundle. Das war die unangenehmste Falle, weil alle Prüfungen grün waren. Das alte PHP aus dem Paket-Repository war gegen das OpenSSL des Basissystems gebaut und nutzte dessen Zertifikatsspeicher /etc/ssl/cert.pem. Mein selbst gebautes PHP ist wegen DEFAULT_VERSIONS+=ssl=openssl40 in der /etc/make.conf gegen das OpenSSL aus den Ports gebaut und sucht seine CA-Zertifikate unter /usr/local/openssl/cert.pem. Diese Datei gab es nicht. Die Folge war, dass jede ausgehende TLS-Verbindung aus PHP mit Zertifikatsprüfung scheiterte. Aufgefallen ist das an einer PHP-Anwendung, die per TLS mit einem IMAP-Server spricht und nur noch „Connection to storage server failed“ meldete. nginx -t, php -m und php-fpm -t waren alle zufrieden, erst der Funktionstest hat es gezeigt.

php -r 'var_dump(openssl_get_cert_locations());'
# default_cert_file => /usr/local/openssl/cert.pem
ln -s /etc/ssl/cert.pem /usr/local/openssl/cert.pem

Alternativ lässt sich der Pfad auch per openssl.cafile in der php.ini setzen.

War schon jemand drin?

Nach dem Patch bleibt die Frage, ob die Lücke vorher schon ausgenutzt wurde. Hier hilft, dass der Angriff laut ist: Er braucht tausende Anfragen, die jeweils einen memory_limit-Abbruch oder einen Fatal Error auslösen. Selbst ein am Ende erfolgreicher Angriff hinterlässt also vorher viele Fehlversuche. Ich habe den gesamten vorhandenen Log-Zeitraum von 15 Tagen durchsucht, rotierte und mit bzip2 gepackte Logs eingeschlossen:

# Abstürze von FPM-Workern
for f in /var/log/php-fpm.log*; do bzcat -f "$f"; done | grep -E 'signal|SIGSEGV|core dumped'

# Speicher- und Fatal-Errors, PHP-FPM ohne eigenes error_log schreibt ins nginx-Errorlog
for f in /var/log/nginx/*error.log*; do bzcat -f "$f"; done | grep -aE 'Allowed memory size|PHP Fatal'

# Neue oder geänderte PHP-Dateien im Webroot
find /usr/local/www -type f \( -name '*.php*' -o -name '*.phar' -o -name '.user.ini' \) -newermt 2026-10-14

Das Ergebnis:

  • Keine abgestürzten FPM-Worker, kein SIGSEGV, kein Core-Dump.
  • Kein einziges Allowed memory size und kein PHP Fatal, weder im FPM-Log noch in den nginx-Errorlogs.
  • Keine Häufung von 5xx-Antworten und keine einzelne Quelle mit auffällig vielen POST-Anfragen. Die meisten POSTs kamen von der ActivityPub-Federation und von wp-cron.
  • Keine fremden Prozesse und keine unbekannten ausgehenden Verbindungen des Webserver-Benutzers, die Crontab enthält nur bekannte Einträge.
  • Im Webroot nur fünf neue WordPress-Sprachdateien. Ihr Zeitstempel passt zu einem wp-cron-Lauf im Access-Log, und der Inhalt ist das übliche <?php return [...] ohne eval, base64_decode oder Zugriffe auf Superglobals. In den Upload-Verzeichnissen liegt gar keine PHP-Datei.

Der einzige Treffer mit Exploit-Charakter war ein Massenscanner, der den alten pearcmd-Trick gegen PHP-CGI ausprobiert hat. Das ist eine ganz andere, alte Lücke, hat mit php2shell nichts zu tun und läuft gegen PHP-FPM ins Leere.

Zur Ehrlichkeit gehören auch die Grenzen dieser Prüfung. Access-Logs enthalten keine POST-Bodies, ein einzelner, gezielter Treffer mit wenigen Anfragen wäre so nicht zu erkennen. Wegen der nötigen Menge an Anfragen halte ich das aber für unwahrscheinlich. Außerdem reichen meine Logs nur 15 Tage zurück. Eine erfolgreiche Codeausführung hätte mit den Rechten des Webserver-Benutzers stattgefunden, die Logs gehören root und hätten von dort aus nicht verändert werden können. Mein Fazit ist deshalb: Es gibt keinen Hinweis auf Ausnutzungsversuche oder eine Kompromittierung. Das heißt „sehr unwahrscheinlich“, nicht „bewiesen ausgeschlossen“.

Was du prüfen solltest

Das PHP-Team hat zusätzlich Konfigurationsänderungen veröffentlicht, die den Angriff erschweren sollen, etwa memory_limit=-1, ein kleines post_max_size, ein niedriges max_input_nesting_level und bei FPM ein niedriges pm.max_requests. Das Team nennt das selbst eine temporäre Krücke. Diese Einstellungen können Funktion oder Leistung kosten, und keine davon schließt die Lücke. Komplett hilft nur das Update. Denk dabei auch an PHP, das du nicht direkt siehst:

  • Docker-Container bringen ihr eigenes PHP mit. Ein Update des Hosts hilft da nicht, das Image muss neu gezogen und der Container neu gestartet werden.
  • Geräte mit Weboberfläche wie pfSense, OPNsense, NAS-Systeme von Synology oder QNAP und viele Router nutzen PHP für ihre Verwaltung. Hier bist du auf Updates des Herstellers angewiesen. Bis dahin gehört so eine Oberfläche erst recht nicht ins Internet.
  • Gehostete Seiten hängen vom Hoster ab. Schau nach, ob er das Update schon eingespielt hat oder einen Termin nennt.
  • Alte PHP-Versionen bekommen teilweise Backports, das ist aber eine gute Gelegenheit, mindestens auf PHP 8.3 zu wechseln. PHP 8.2 bekommt nur noch bis Ende 2026 Sicherheitsupdates.
  • Auf FreeBSD lohnt sich bei kritischen Lücken der Blick in den Ports-Tree, bevor man auf das Paket wartet. Prüf dann aber auch, gegen welches OpenSSL der Port gebaut wird.

Mein Fazit

nginx abzuschalten war die einfachste Maßnahme, die garantiert wirkt, und ein paar Stunden ohne Blog tun niemandem weh. Das eigentliche Patchen hat mich mehr gelehrt als die Lücke selbst: Eine neue Versionsnummer ist noch kein Beweis für den richtigen Fix, ein neues Binary auf der Platte noch kein gepatchter Dienst, und eine grüne Konfigurationsprüfung noch kein funktionierendes System. Erst der Funktionstest und der Blick in die Logs geben am Ende ein ehrliches Bild.

Nachtrag 23. Oktober 2026: Wer die weitere Entwicklung zu php2shell verfolgen will, findet bei pwn.watch eine laufend aktualisierte Zeitleiste mit Patch-Status, Advisories und Feeds pro Stack. Mehr dazu in pwn.watch: Sicherheitsvorfälle im Überblick, mit Vorsicht beim Brief für die AI.

Kritische PHP-Lücken: mein Blog geht bis zum Update offline

Warndreieck neben dem Schriftzug PHP, kritische Sicherheitslücken, Update am 21.10.2026 um 16:00 Uhr, Blog bis zum Patch offline

Kurze Info in eigener Sache: Heute, am 21. Oktober 2026, veröffentlicht das PHP-Projekt um 16:00 Uhr unserer Zeit (14:00 Uhr UTC) ein Sicherheitsupdate für zwei kritische Lücken. heise security hat das in der Meldung Wichtige PHP-Updates am 21. Oktober beheben Codeeinschleusung-Schwachstellen zusammengefasst, und die Lage klingt ernst genug, dass ich meinen Blog vorerst vom Netz nehme.

Was bisher bekannt ist

Das PHP-Projekt hat das Update am Vortag auf php.net angekündigt. Beide Lücken sind mit CVSS 4.0 von 9.2 bewertet, betroffene und reparierte Versionen sollen erst mit dem Release genannt werden. Das CERT-EU warnt ebenfalls und spricht von möglicher Codeausführung aus der Ferne ohne Anmeldung.

Laut heise stecken die Fehler in jeder PHP-Version seit PHP 5. Ein bereits kursierender Proof of Concept zielt zwar nur auf die Server-API von LiteSpeed, darauf würde ich mich aber nicht verlassen. Auch die herumgereichte Gegenmaßnahme session.upload_progress.enabled = 0 soll nach Kenntnisstand von heise nicht helfen. Funktionierende Exploits erwartet heise innerhalb von Stunden, spätestens Tagen. Wer kein Risiko eingehen will, hat bis zum Update also nur die Wahl, PHP-Webserver vom Netz zu nehmen.

Warum mein Blog offline geht

Dieser Blog läuft mit WordPress und damit mit PHP auf einem FreeBSD-Server. Selbst wenn das Update um 16 Uhr erscheint, dauert es noch eine Weile, bis die gepatchten Pakete in den FreeBSD-Ports und im Paket-Repository landen und ich sie eingespielt habe. Für dieses Zeitfenster möchte ich keinen Server offen im Netz haben, auf dem jeder anonym Code ausführen könnte. Ein paar Stunden oder auch Tage ohne Blog sind ärgerlich, ein kompromittierter Server wäre deutlich ärgerlicher.

Ich nehme den Blog daher jetzt erst einmal offline. Sobald die reparierten PHP-Pakete installiert sind und ich mir die technischen Details angesehen habe, kommt er wieder zurück. Wer mir im Fediverse oder auf Bluesky folgt, bekommt diesen Beitrag trotzdem zu sehen, auch wenn die Seite selbst gerade nicht erreichbar ist.

Was du jetzt tun solltest

Wenn du selbst PHP-Anwendungen im Netz betreibst, schau jetzt nach, wo überall PHP läuft. Gerade bei vergessenen Testinstanzen, alten Foren oder einem phpMyAdmin wird es schnell unübersichtlich. Behalte die Sicherheitsmeldungen deiner Distribution im Blick und spiel die Updates ein, sobald sie da sind. Was davon du bis dahin vom Netz nimmst, musst du für deine eigenen Systeme abwägen.

Update 21. Oktober 2026: Der Blog ist wieder online. Was hinter php2shell steckt und wie ich PHP noch am selben Tag aus den FreeBSD-Ports gepatcht habe, steht im Folgebeitrag php2shell: PHP am Tag der Veröffentlichung aus den FreeBSD-Ports gepatcht.

Orca: eine ADE, in der mehrere Coding-Agenten sauber nebeneinander arbeiten

Illustration von Orca als Agent Development Environment: Vier KI-Coding-Agenten arbeiten parallel in getrennten Git-Worktrees an Code, Benutzeroberfläche, API und Tests. Ein Entwickler überprüft die Änderungen und führt sie zusammen.

Seit fast fünf Monaten arbeite ich mit Orca, und es ist in der Zeit zu einem Werkzeug geworden, das ich jeden Tag offen habe. Ich möchte es euch deshalb kurz vorstellen. Das hier ist kein großer Testbericht, eher eine Empfehlung mit ein paar Erklärungen dazu, was so ein Tool überhaupt ist und warum ich gerade bei diesem hängen geblieben bin.

Was ist eine ADE?

Eine IDE kennt jeder, der programmiert: Editor, Terminal, Debugger und Git in einem Programm. Sie ist für einen Menschen gebaut, der selbst tippt. Mit Coding-Agenten wie Claude Code oder Codex hat sich die Arbeit aber verschoben. Ich schreibe weniger Code selbst, ich gebe Aufgaben ab, schaue mir an, was zurückkommt, korrigiere und lasse weiterlaufen. Dafür ist eine klassische IDE nicht gemacht.

ADE steht für Agent Development Environment. Orca beschreibt das selbst ganz treffend: Eine IDE ist für dich gebaut, eine ADE für dich und deine Agenten. Im Mittelpunkt steht also nicht mehr der Editor, sondern die Frage, wie man mehrere Agenten gleichzeitig arbeiten lässt, ohne den Überblick zu verlieren. Was macht welcher Agent gerade, wer wartet auf eine Freigabe, wer ist fertig, und was hat er eigentlich geändert?

Der technische Kern dabei sind Git-Worktrees. Ein Worktree ist ein zusätzliches Arbeitsverzeichnis desselben Repositories mit eigenem Branch. Lässt man zwei Agenten im selben Verzeichnis arbeiten, schreiben sie sich gegenseitig in die Dateien, und spätestens beim Testlauf weiß keiner mehr, welche Änderung von wem kommt. Bekommt jeder Agent seinen eigenen Worktree, arbeiten sie sauber getrennt nebeneinander, und am Ende entscheide ich, was gemergt wird. Ohne Werkzeug ist das allerdings viel Handarbeit mit git worktree add, mehreren Terminals und dem Gefühl, ständig irgendwo etwas zu vergessen.

Was Orca besonders macht

Genau diese Handarbeit nimmt mir Orca ab. Ich lege eine Aufgabe an, Orca erstellt den Worktree, startet darin den Agenten und zeigt mir in der Seitenleiste alle laufenden Aufgaben über alle Projekte hinweg. Die Punkte, die Orca für mich von anderen Lösungen abheben:

  • Jeder CLI-Agent, jedes eigene Abo. Orca bringt keinen eigenen Agenten und kein eigenes Modell mit. Claude Code, Codex, Gemini, OpenCode und viele weitere sind vorkonfiguriert, und alles, was im Terminal läuft, lässt sich einbinden. Dabei nutze ich die Abos, die ich ohnehin habe. Ich bin also nicht an einen Anbieter gebunden und muss auch kein zusätzliches Abo abschließen.
  • Parallele Worktrees. Jede Aufgabe läuft isoliert. Man kann sogar denselben Prompt an mehrere Agenten geben, die Ergebnisse vergleichen und den besten Stand übernehmen.
  • Diffs kommentieren. Änderungen eines Agenten sehe ich als Diff, kann direkt an einzelne Zeilen Kommentare hängen und diese gesammelt an den Agenten zurückgeben. Das fühlt sich an wie ein Code-Review mit einem Kollegen und ist deutlich angenehmer, als jede Anmerkung einzeln in den Prompt zu tippen.
  • Terminals, Editor und Browser in einem Fenster. Terminals lassen sich beliebig teilen, der Verlauf übersteht einen Neustart. Für Web-Projekte gibt es pro Worktree ein eigenes Chromium-Fenster, in dem man ein Element anklicken und es samt HTML, CSS und Bildausschnitt an den Agenten schicken kann.
  • Worktrees per SSH. Die Agenten müssen nicht auf dem eigenen Rechner laufen. Orca kann Worktrees auf einer entfernten Maschine anlegen und dort Dateien, Git und Terminals bedienen.
  • Verbrauch im Blick. Orca zeigt die Nutzung von Claude und Codex an, inklusive der Zeitpunkte, an denen die Limits zurückgesetzt werden. Bei mehreren parallelen Agenten ist das Gold wert, weil die Kontingente sonst schneller weg sind, als man denkt.
  • Eine Begleit-App fürs Handy. Für iOS und Android gibt es eine App, mit der man den Status der Agenten sieht und auch unterwegs eine Rückfrage beantworten kann.

Dazu kommt etwas, das für mich nicht ganz unwichtig ist: Orca ist Open Source unter MIT-Lizenz und läuft auf Linux genauso wie auf macOS und Windows. Für Linux gibt es Pakete als .deb, .rpm und AppImage, jeweils für x86_64 und ARM64, dazu ein Paket im AUR. Dahinter steht das Startup Stably AI. Das Repository gibt es erst seit März 2026 und es hat inzwischen knapp 96.000 Sterne auf GitHub. Es scheint also nicht nur mir so zu gehen.

Was du wissen solltest

Orca sammelt anonyme Nutzungsdaten. Laut der Doku zur Telemetrie sind das Ereignisse wie App-Starts, neu angelegte Workspaces oder gestartete Agenten, verknüpft mit einer zufälligen lokalen ID. Dateiinhalte, Prompts, Ausgaben der Agenten sowie Repo- und Branch-Namen werden nach eigener Aussage nicht übertragen. Wer das trotzdem nicht möchte, schaltet es unter Settings > Privacy ab oder startet Orca mit einer dieser Umgebungsvariablen:

DO_NOT_TRACK=1
ORCA_TELEMETRY_DISABLED=1

Außerdem ist das Entwicklungstempo enorm. Neue Versionen kommen fast täglich, die Versionsnummer steht inzwischen bei 1.4.229. Das bringt laufend neue Funktionen, heißt aber auch, dass sich hin und wieder etwas verändert oder kurz hakt. Wer ein Werkzeug sucht, das sich über Monate nicht bewegt, ist hier falsch.

Und eine ADE nimmt dir das Denken nicht ab. Mehrere Agenten parallel arbeiten zu lassen, heißt auch, mehrere Ergebnisse parallel prüfen zu müssen. Orca macht das Prüfen deutlich bequemer, die Verantwortung für das, was am Ende gemergt wird, bleibt aber bei dir. Warum ich das für einen der wichtigsten Punkte beim Arbeiten mit AI halte, habe ich in AI, der Mensch als Flaschenhals schon einmal aufgeschrieben.

Mein Fazit

Wer nur ab und zu einen Agenten im Terminal startet, braucht Orca nicht unbedingt. Sobald aber mehrere Aufgaben gleichzeitig laufen, vielleicht sogar mit verschiedenen Agenten, wird es ohne passendes Werkzeug schnell unübersichtlich. Genau da spielt Orca seine Stärken aus. Es ist kostenlos, offen und hängt nicht an einem einzelnen Anbieter. Probiere es einfach mal aus, die Downloads gibt es auf onorca.dev.

Siehe auch:

Wenn ihr Fragen habt, dann dürft ihr mich sehr gerne fragen.

« Ältere Beiträge

© 2026 -=Kernel-Error=-

Theme von Anders Norén — Hoch ↑