Artikel aus edv

  • Impfpass ohne App, Apple und Google

    Morgen werden zwei Wochen seit meiner zweiten Corona-Impfung vergangen sein. Damit wird die Impfpassfrage für mich relevant: Mit so einem Ding könnte ich wieder in die Mensa gehen!

    Allerdings habe ich, soweit ich das sehe, keinen Zugang zur offiziellen Covpass-App, weil ich mich von Apples Appstore und Googles Playstore fernhalte und eigentlich auch die Toolchains der beiden nicht auf meinem Rechner haben will. Immerhin gibt es (es lebe der Datenschutz-Aktivismus, der für offene Entwicklung von dem Kram gesorgt hat) die Quellen der Apps (wenn auch leider auf github). Wie kompliziert kann es schon sein, das ohne den ganzen proprietären Zauber nachzubauen?

    Stellt sich raus: schon etwas, aber es ist ein wenig wie eine Kurzgeschichte von H.P. Lovecraft: Die Story entwickelt sich wie beim Schälen einer Zwiebel. Schale um Schale zeigt sich, dass die Wahrheit immer noch tiefer liegt.

    Bei Lovecraft sind nach dem Abpulen der letzten Schale meist alle ProtagonistInnen tot oder wahnsinnig. Im Fall der Covpass-App hingegen ist der Kram sogar dokumentiert: So finden sich die halbwegs lesbare Dokumentation des Datenstroms im QR-Code und das JSON-Schema – leider schon wieder auf github.

    Schale 1: QR-Code scannen

    Ich dachte mir, zum Nachbauen der Covpass-App sollte ich erstmal ihre Eingabe verstehen, also die Daten aus den beiden Impfzertifikaten lesbar darstellen. Der erste Schritt dazu ist etwas, das QR-Codes lesen kann. Ich hatte anderweitig schon mit zbar gespielt, für das es das Debian-Paket python3-zbar gibt. Damit (und mit der unverwüstlichen Python Imaging Library aus python3-pillow) geht das so, wenn das Foto mit dem Zertifikat in der Datei foto.jpeg liegt:

    def get_single_qr_payload(img):
      img = img.convert("L")
    
      scanner = zbar.ImageScanner()
      scanner.parse_config('enable')
      image = zbar.Image(img.size[0], img.size[1], 'Y800', data=img.tobytes())
      if not scanner.scan(image):
        raise ValueError("No QR code found")
    
      decoded = list(image)
      if len(decoded)>1:
        raise ValueError("Multiple QR codes found")
    
      return decoded[0].data
    
    encoded_cert = get_single_qr_payload(Image.open("foto.jpeg"))
    

    Im Groben wandele ich in der Funktion das Bild (das wahrscheinlich in Farbe sein wird) in Graustufen, denn nur damit kommt zbar zurecht. Dann baue ich eine Scanner-Instanz, also das Ding, das nachher in Bildern nach QR-Codes sucht. Die API hier ist nicht besonders pythonesk, und ich habe längst vergessen, was parse_config('enable') eigentlich tut – egal, das ist gut abgehangene Software mit einem C-Kern, da motze ich nicht, noch nicht mal über diesen fourcc-Unsinn, mit dem vor allem im Umfeld von MPEG allerlei Medienformate bezeichnet werden; bei der Konstruktion des zbar.Image heißt „8 bit-Graustufen“ drum "Y800". Na ja.

    Der Rest der Funktion ist dann nur etwas Robustheit und wirft ValueErrors, wenn im Foto nicht genau ein QR-Code gefunden wurde. Auch hier ist die zbar-API vielleicht nicht ganz preiswürdig schön, aber nach dem Scan kann mensch über zbar.Image iterieren, was die verschiedenen gefundenen Barcodes zurückgibt, zusammen mit (aus meiner Sicht eher knappen) Metadaten. Das .data-Attribut ist der gelesene Kram, hier als richtiger String (im Gegensatz zu bytes, was ich hier nach der python3-Migration eher erwartet hätte).

    Schale 2: base45-Kodierung

    Das Ergebnis sieht nach üblichem in ASCII übersetzten Binärkram aus. Bei mir fängt der etwa (ich habe etwas manipuliert, das dürfte so also nicht dekodieren) so an: HC1:6B-ORN*TS0BI$ZDFRH%. Insgesamt sind das fast 600 Zeichen.

    Als ich im Wikipedia-Artikel zum Digitalen Impfnachweis gelesen habe, das seien base45-kodierte Daten, habe ich erst an einen Tippfehler gedacht und es mit base85 versucht, das es in Pythons base64-Modul gibt. Aber nein, weit gefehlt, das wird nichts. War eigentlich klar: die Wahrscheinlichkeit, dass was halbwegs Zufälliges base85-kodiert keine Kleinbuchstaben enthält, ist echt überschaubar. Und base45 gibts wirklich, seit erstem Juli in einem neuen RFC-Entwurf, der sich explizit auf QR-Codes bezieht. Hintergrund ist, dass der QR-Standard eine Kodierungsform (0010, alphanumeric mode) vorsieht, die immer zwei Zeichen in 11 bit packt und dafür nur (lateinische) Großbuchstaben, Ziffern und ein paar Sonderzeichen kann. Extra dafür ist base45 erfunden worden. Fragt mich bloß nicht, warum die Leute nicht einfach binäre QR-Codes verwenden.

    Es gibt bereits ein Python-Modul für base45, aber das ist noch nicht in Debian bullseye, und so habe ich mir den Spaß gemacht, selbst einen Dekodierer zu schreiben. Technisch baue ich das als aufrufbares (also mit einer __call__-Methode) Objekt, weil ich die nötigen Tabellen aus dem globalen Namensraum des Skripts draußenhalten wollte. Das ist natürlich ein Problem, das verschwindet, wenn sowas korrekt in ein eigenes Modul geht.

    Aber so gehts eben auch:

    class _B45Decoder:
      chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:"
      code_for_char = dict((c, i) for i, c in enumerate(chars))
      sig_map = [1, 45, 45*45]
    
      def __call__(self, str):
        raw_bytes = [self.code_for_char[s] for s in str]
        cooked_bytes = []
    
        for offset in range(0, len(raw_bytes), 3):
          next_raw = raw_bytes[offset:offset+3]
          next_bytes = sum(w*v for w,v in
            zip(self.sig_map, next_raw))
          if len(next_raw)>2:
            cooked_bytes.append(next_bytes//256)
          cooked_bytes.append(next_bytes%256)
    
        return bytes(cooked_bytes)
    
    b45decode = _B45Decoder()
    

    Die b45decode-Funktion ist, wenn mensch so will, ein Singleton-Objekt der _B45Decoder-Klasse (so hätten das jedenfalls die IBMlerInnen der CovPass-App beschrieben, vgl. unten). Ansonsten bildet das den Grundgedanken von base45 ziemlich direkt ab: Immer drei Bytes werden zur Basis 45 interpretiert, wobei die Wertigkeit der verschiedenen Zeichen im Dictionary code_for_char liegt. Die resultierende Zahl lässt sich in zwei dekodierte Bytes aufteilen. Nur am Ende vom Bytestrom muss mensch aufpassen: Wenn die dekodierte Länge ungerade ist, stehen kodiert nur zwei Byte.

    Und ja: vielleicht wärs hübscher mit dem grouper-Rezept aus der itertools-Doku, aber die next_raw-Zuweisung schien mir klar genug.

    Schale 3: zlib

    Ein b45decode(encoded_cert) gibt erwartungsgemäß wilden Binärkram aus, etwas wie b'x\x9c\xbb\xd4\xe2\xbc\x90Qm!… Das sind, ganz wie die Wikipedia versprochen hat, zlib-komprimierte Daten. Ein zlib.decompress wirkt und führt auf etwas, in dem zum ersten Mal irgendetwas zu erkennen ist; neben viel Binärkram findet sich etwa:

    ...2bdtj2021-07-01bistRobert Koch-InstitutbmamOR...
    

    Keine Frage: Ich mache Fortschritte.

    Schale 4: CBOR/COSE

    An dieser Stelle bin ich auf mir unbekanntes Terrain vorgestoßen: CBOR, die Concise Binary Object Representation (Nerd-Humor: erfunden hat das Carsten Bormann, weshalb ich auch sicher bin, dass das Akronym vor der ausgeschriebenen Form kam) alias RFC 7049. Gedacht ist das als so eine Art binäres JSON; wo es auf die Größe so ankommt wie bei QR-Codes, habe ich schon Verständnis für diese Sorte Sparsamkeit. Dennoch: Sind Protocol Buffers eigentlich tot?

    Praktischerweise gibt es in bullseye das Paket python3-cbor2. Munter habe ich cbor2.loads(raw_payload) geschrieben und war doch etwas enttäuscht, als ich etwas wie:

    CBORTag(18, [b'\xa1\x01&',
      {4: b'^EVf\xa5\x1exW'},
      b'\xa4\x01bDE...',
      b'\xf9\xe9\x9f...'])
    

    zurückbekommen habe. Das ist deutlich mehr viel Binärrauschen als ich erhofft hatte. Aber richtig, das Zeug sollte ja signiert sein, damit die Leute sich ihre Impf- und Testzertifikate nicht einfach selbst schreiben. Die hier genutzte Norm heißt COSE (nämlich CBOR Object Signing and Encryption, RFC 8152 aus dem Jahr 2017). Ich habe das nicht wirklich gelesen, aber Abschnitt 2 verrät gleich mal, dass, wer an einer Signaturprüfung nicht interessiert ist (und das bin ich nicht, solange ich nicht vermuten muss, dass meine Apotheke mich betrogen hat), einfach nur aufs dritte Arrayelement schauen muss. Befriedigenderweise ist das auch das längste Element.

    Ein wenig neugierig war ich aber schon, was da noch so drinsteht. Die 18 aus dem CBORTag heißt nach Abschnitt 4.2, dass das eine Nachricht mit nur einer Unterschrift ist, und der letzte Binärkram ist eben diese eine Unterschrift. Das erste Array-Element sind Header, die mit unterschrieben werden, wieder CBOR-kodiert. Dekodiert ist das {1: -7}, und Überfliegen der COSE-Spezifikation (Tabellen 2 und 5) schlägt vor, dass das heißt: der Kram ist per ECDSA mit einem SHA-256-Hash unterschrieben.

    Tabelle 2 von COSE erklärt auch das nächste Element, die Header, die nicht unterschrieben werden (und über die mensch also Kram in die Nachrichten einfummeln könnte). Das sieht auch erstmal binär aus, ist aber ein „entpacktes“ Dictionary: In meiner Nachricht steht da nur ein Header 4, was der „Key Identifier“ ist. Der Binärkram ist schlicht eine 64-bit-Zahl, die angibt, mit welchem Schlüssel die Unterschrift gemacht wurde (bei PGP wären das die letzten 8 byte des Fingerabdrucks, viel anders wird das bei COSE auch nicht sein).

    Schale 5: CBOR lesbar machen

    Zurück zu den Zwiebelschalen. Wenn also die Nutzdaten im dritten Element des Array von oben sind, sage ich:

    cbor_payload = cbor2.loads(cose_payload.value[2])
    

    Heraus kommt dabei etwas wie:

    {1: 'DE', 4: 1657705736, 6: 1626169736,
    -260: {1: {'v': [{'co': 'DE', 'dn': 2, 'dt': '2021-07-01',
    'is': 'Robert Koch-Institut', 'ma': 'ORG-100030215',
    'mp': 'EU/1/20/1528', 'sd': 2, 'tg': '840539006',...}]}}}
    

    – das ist ziemlich offensichtlich die Datenstruktur, die der Zauber liefern sollte. Nur sind die Schlüssel wirklich unklar. v könnte wohl „Vaccination“ sein, is der Issuer, also der Herausgeber des Impfpasses; die Werte von 4 und 6 sehen verdächtig nach Unix-Timestamps in der nächsten Zeit aus (ja, es sind schon sowas wie 1,6 Milliarden Sekunden vergangen seit dem 1.1.1970).

    Aber Raten ist doof, wenn es Doku gibt. Wie komme ich also zu …

  • Javascript Local Storage

    Es gibt eine große Zahl gruseliger APIs (also: „Kram, den ein Programm tun kann“) in modernem Javascript, von Sensor APIs (zur Abfrage von Beschleunigung und Orientierung des Geräts und des Umgebungslichts) bis Websocket (mit dem weitgehend beliebige Server dauernd mit dem Client reden können, und das auch noch „im Hintergrund“ in Web Workers) – gruselig ist das, weil in aktuellen und nicht weiter modifizierten Browsern jede Webseite dieses Zeug nutzen kann, wobei eingestandenermaßen ein paar besonders dramatische APIs (Mikrofon und Kamera z.B.) noch durch Rückfragen abgesichert sind. Oh: Webseiten können das nicht nur nutzen, viel zu viele brauchen Javascript und gehen nicht, wenn mensch es abdreht.

    Der breite Zugriff auf fast alles von Javascript aus ist der Grund, warum ich des Öfteren ziemlich unwillig auf „please enable Javascript“-Banner (und noch mehr auf ihre Abwesenheit trotz Abhängigkeit von Javascript) reagiere. Javascript erlauben bedeutet spätestens seit XMLHTTPRequest (mit dem Javascript an der NutzerIn vorbei mit dem Ursprungsserver kommunizieren konnte; kam in der ersten Hälfte der Nullerjahre) und CORS (was XMLHTTPRequest und jetzt fetch auf beliebige Server ausweitete, solange die kooperieren; im Firefox tauchte das 2009 auf) einen recht weitgehenden Zugriff auf die eigene Maschine zu erlauben, und das noch ganz ohne Spectre und Freunde – die übrigens ohne ubiquitäres Javascript auf privaten Rechnern weitgehend bedeutungslos wären.

    Eine API, die ziemlich gruselig ist, weil sie Webseiten ein unendlich langes Gedächtnis in eurem Browser gibt, ist Local Storage; damit können Webseiten ernsthafte Datenmengen in eurem Browser speichern und sie wiederfinden, wenn sie das nächste Mal Javascript ausführen dürfen. Dass das zunächst lokal gespeichert wird, hilft nicht viel – per Websocket oder zur Not einem fetch mit Payload ist der Kram auch (wieder) auf jedem Server, der bereit ist, sich das anzuhören. Wohlgemerkt: ohne dass NutzerInnen irgendwas mitbekommen.

    Wenn ihr mein Gruseln nachfühlen wollt, könnt ihr hier mal Javascript einschalten (ihr lasst mich doch sonst kein Javascript ausführen, oder?). Ihr bekommt dann unter diesem Absatz ein Eingabefeld, in das ihr Kram tippen könnt. Wenn ihr einen hinreichend modernen Browser habt (technisch: er muss das storage-Signal schicken; Firefox 78 tut das z.B., Webkit 4.0.37 nicht), könnt ihr eure Nachricht in der Javascript-Warnung am Fuß der Seite sehen, schockierenderweise auch in anderen Fenstern, die ihr auf blog.tfiu.de offen habt. Auf allen halbwegs aktuellen Großbrowsern erscheint der Text jedenfalls nach dem nächsten Reload. Und bleibt auch da. Aufauf, schreibt eurem künftigen Selbst eine Nachricht in den Fuß dieser Seite:

    Gut. Du hast Javascript aus.

    Nennt mich paranoid, aber lustig ist das nicht.

    Und so ärgern mich noch viel mehr als Seiten, die Javascript fordern, Seiten, die ohne Local Storage leer bleiben oder sonst irgendwie undurchschaubar kaputt gehen.

    Was tun?

    Für die großen Browser gibt es allerlei Erweiterungen, die das Management von Local Storage erlauben; im Firefox sehe ich gerade Forget Me Not oder StoragErazor (was gleich noch die IndexedDB-API mit abdeckt, die ähnlich schrecklich ist, aber noch nicht viel genutzt wird); soweit ich erkennen kann, erlaubt das unverzichtbare noscript nicht, Javascript ohne Local Storage laufen zu lassen.

    Für meinen Haupt-Browser, den Luakit (verdammt, schon wieder ein Link von hier in github rein), habe ich eine kleine Erweiterung geschrieben, mit der ich mit der Tastenkombination ,tq schnell local storage ein- und ausschalten kann; auf die Weise kann ich normalerweise ohne Local Storage browsen, wenn dann aber mal wirklich eine Seite kaputt ist (gitlab ist da derzeit so ein ganz schlechtes Beispiel), braucht es nur ein paar Anschläge. Eine Anzeige in der Fußleiste (q/Q für Storage aus/an) kommt auch mit:

    -- Web storage control
    
    -- @module webstorage_control
    -- @copyright Public Domain
    
    local _M = {}
    
    local window = require("window")
    local theme = require("theme")
    local settings = require("settings")
    local modes = require("modes")
    local add_binds = modes.add_binds
    
    function update_webstorage_disp(w)
        if settings.get_setting("webview.enable_html5_database") then
            w.sbar.r.webstorage_d.text = "Q"
        else
            w.sbar.r.webstorage_d.text = "q"
        end
    end
    
    function toggle_local_storage(w)
        local local_storage_enabled =
            settings.get_setting("webview.enable_html5_database")
    
        settings.set_setting("webview.enable_html5_database",
            not local_storage_enabled)
        settings.set_setting("webview.enable_html5_local_storage",
            not local_storage_enabled)
        update_webstorage_disp(w)
    end
    
    window.add_signal("init", function (w)
        local r = w.sbar.r
        r.webstorage_d = widget{type="label"}
        r.layout:pack(r.webstorage_d)
        r.layout:reorder(r.webstorage_d, 1)
        r.webstorage_d.font = theme.font
        update_webstorage_disp(w)
    end)
    
    add_binds("normal", {
        { "^,tq$", "Enable/disable web local storage", toggle_local_storage},
    })
    
    return _M
    
    -- vim: et:sw=4:ts=8:sts=4:tw=80
    

    Wer auch den luakit verwendet, kann das nach .config/luakit/webstorage_control.lua packen und dann:

    require("webstorage_control")
    

    an geeigneter Stelle (z.B. in .config/luakit/rc.lua) unterbringen. Wenn dermaleinst so viele Seiten ohne Local Storage kaputtgehen wie derzeit ohne Javascript, müsste das wahrscheinlich eher wie in der noscript-Erweiterung automatisiert werden.

    Auch wenn ich mal Local Storage erlaube, will ich natürlich nicht, dass der Kram persistent bleibt. Deshalb habe ich noch folgendes kleine Python-Programm geschrieben:

    #!/usr/bin/python
    """
    Clear luakit web local storage and cookies
    
    There's a whitelist applied to both.
    """
    
    
    import fnmatch
    import glob
    import os
    import re
    import sqlite3
    
    
    class Whitelist:
        """A fnmatch-based whitelist.
    
        Test as in "domain in whitelist".  It's not fast, though.
        """
        def __init__(self,
                src_path=os.path.expanduser("~/.config/luakit/cookie.whitelist")):
            with open(src_path) as f:
                self.patterns = [s.strip() for s in f.read().split("\n")]
    
        def __contains__(self, domain):
            for pattern in self.patterns:
                if fnmatch.fnmatch(domain, pattern):
                    return True
            return False
    
    
    def clear_cookies(whitelist):
        """removes cookies from domains not in whitelist from luakit's
        cookies db.
        """
        conn = sqlite3.connect(
            os.path.expanduser("~/.local/share/luakit/cookies.db"))
    
        try:
            all_hosts = list(r[0]
                for r in conn.execute("select distinct host from moz_cookies"))
            for host in all_hosts:
                if host in whitelist:
                    continue
                conn.execute("delete from moz_cookies where host=?",
                    (host,))
        finally:
            conn.commit()
            conn.execute("VACUUM")
            conn.close()
    
    
    def try_unlink(f_name):
        """removes f_name if it exists.
        """
        if os.path.exists(f_name):
            os.unlink(f_name)
    
    
    def clear_local_storage(whitelist):
        """removes luakit's local storage files unless their source
        domains are whitelisted for cookies.
        """
        for f_name in glob.glob(os.path.expanduser(
                "~/.local/share/luakit/local_storage/*.localstorage")):
            mat = re.match("https?_(.*?)_\d+.localstorage",
                os.path.basename(f_name))
            if not mat:
                print("{}???".format(f_name))
                continue
    
            if mat.group(1) in whitelist:
                continue
    
            try_unlink(f_name)
            try_unlink(f_name+"-shm")
            try_unlink(f_name+"-wal")
    
    
    def main():
        whitelist = Whitelist()
        clear_cookies(whitelist)
        clear_local_storage(whitelist)
    
    
    if __name__=="__main__":
        main()
    

    Das Programm liest eine Liste von Shell-Patterns (also etwas wie *.xkcd.com) aus einer Datei ~/.config/luakit/cookie.whitelist und löscht dann alle Cookies und Local Storage-Einträge im Luakit, die nicht von Servern kommen, die in dieser Ausnahmeliste erwähnt sind. Das Ganze läuft bei mir jeden Morgen aus meiner Crontab:

    01 09 * * * ~/mybin/clear_luakit_cookies.py
    

    Aber: Besser wärs, das wäre alles nicht da. In einem Browser, also etwas, mit dem mensch Webseiten anschauen will, hat eine API wie Local Storage mit Persistenz und Signalisierung eigentlich nichts verloren.

    Oh: Der Javascript-Quellcode für den ganzen Spaß mit euren Notizen in der Fußzeile ist erschreckend klein. Für den Fall, dass ich das mal anders schreibe und das so nicht mehr im Seiten-Quellcode zu finden sein wird, hier zunächst der Code für das Eingabefeld oben:

    <div id="textarea-container">
      <p><em>Gut.  Du hast Javascript aus.
      </em></p>
    </div>
    
    <script deferred="deferred">
      function setUp() {
        document.querySelector("#textarea-container").innerHTML =
          `<textarea id="to-store" style="width:100%; height:6cm"
          placeholder="Tippt kram, den ihr im Fuß der Seite sehen wollt."
          ></textarea>`;
        let textSource = document.querySelector("#to-store");
        if (!window.localStorage) {
          textSource.value = "Ah, gut.  Du hast local storage aus.  Dann\n"
            +"geht das hier nicht.  Wie gesagt, das ist gut.";
           return;
        }
    
        if (window.localStorage.savedText) {
          textSource.value = window.localStorage.getItem("savedText");
        }
    
        textSource.addEventListener("input", () => {
          window.localStorage.setItem("savedText", textSource.value);
        });
    
        window.addEventListener("storage", (ev) => {
          textSource.value = ev.newValue;
        });
      }
    
      setUp();
    </script>
    

    Und dann noch der fürs Management der Fußzeile (der sitzt jetzt gerade im head-Element):

    if (window.localStorage) {
      target.innerHTML +=
        `<div id="ls-warning"><p><strong>Schlimmer noch:</strong>
        Dein Browser lässt mich
        auch local storage machen.  Wenn du dir in
        <a href="/javascript-local-storage.html">diesem Artikel</a>
        eine Nachricht hinterlässt, bekommst du sie bei deinem nächsten
        Besuch unten zu sehen; du kannst da sogar dein eigenes
        HTML und Javascript unterbringen und ausführen lassen.</p>
        <p id="user-message"/></div>`;
    
      if (window.localStorage.savedText) {
        document.querySelector("#user-message").innerHTML =
          window.localStorage.savedText;
      }
    
      window.addEventListener("storage", (ev) => {
        document.querySelector("#user-message").innerHTML = ev.newValue;
      });
    }
    
  • Bluetooth tethering with a 2021 Debian

    The other day the cage that holds the SIM card for the wireless modem of my Lenovo X240 rotted away. Fixing this (provided that's even reasonable, which I'm not sure about) requires digging quite a bit deeper into the machine than I consider proportional for something I use less than once a month. Plus, there's still my trusty N900 cellphone that I can use for the occasional GSM (or, where still available, UMTS) data connection.

    A SIM card cage on a table

    The underlying reason for mucking around tethering bluetooth in 2021: the cage for the SIM card in my computer rotted out. And re-attaching it to the mainboard looks like surgery to deep for summer.

    So, I revived the ancient scripts I used to use around 2005 with feature phones playing cell modem and tried to adapt them to the N900. Ouch.

    Well, full disclosure: I have only hazy notions about what's going on in bluetooth in general, and the Linux tooling for bluetooth I find badly confusing. Hence, rather than reading manpages I started asking duckduckgo for recipes for “bluetooth tethering linux“ or similar. And since what I found was either rather outdated or used various GUI and network management tools I prefer to not have to regularly run, let me write up what I ended up doing to thether my Debian box. Oh: it's using ifupdown and is Debian-specific in that sense, but otherwise I think it's fairly distribution-neutral, contrary to what you might expect after the headline.

    The bluetooth part

    The basic operation for bluetooth tethering is straightforward: Open a serial-like connection (“rfcomm”) to the phone, then start a pppd on top of it. It's the little details that make this tricky.

    The first detail for me is that I have a deep distrust of bluez (and even the bluetooth drivers). I hence keep bluetooth off and blocked most of the time, and before opening any bluetooth connection, I have to manage this bluetooth state, which I'm going to do in a plain shell script. I'm sure there's a very elegant way to do this in systemd, but then I'd say this is a case where the clarity of a shell script is hard to beat.

    So, I created a file /usr/local/sbin/bluenet containing this:

    #!/bin/sh
    # Configure/deconfigure an rfcomm bluetooth connection
    
    DEVICE_MAC=<YOUR PHONE'S BLUETOOTH ID>
    
    case $1 in
      start)
        /usr/sbin/rfkill unblock bluetooth
        /sbin/modprobe btusb
        /usr/sbin/service bluetooth start
        /usr/bin/rfcomm bind /dev/rfcomm0  $DEVICE_MAC
        sleep 2 # no idea what I should really be waiting for here, but
          # bluetooth clearly needs some time to shake out
        ;;
      stop)
        /usr/bin/rfcomm release /dev/rfcomm0
        /usr/sbin/service bluetooth stop
        /sbin/rmmod btusb
        /usr/sbin/rfkill block bluetooth
        ;;
      default)
        echo "$1 start|stop"
        exit 1
    esac
    

    All that you really need if you don't mind having bluetooth on are the two rfcomm command lines; the advantage of having this in a separate script (rather than just hack the rfcomm calls into the ifupdown stanza) is that you can say bluenet stop after a failed connection attempt and don't have to remember what exactly you started and what the stupid rfcomm command line was. Oh: Resist the temptation to keep this script in your home directory; it will be executed as root by ifupdown and should not be writable by your normal user.

    To figure out your phone's bluetooth id, I think what people generally use these days is bluetoothctl (and for interactive use, it's fairly nice, if scantily documented). Essentially, you say scan on in there and wait until you see something looking like your phone (you'll have to temporarily make it discoverable for that, of course). While you're in there, run pair <mac>, too – at least for me, that was really straightforward compared to the hoops you had to jump through to pair bluetooth devices in Linux in the mid-2000s.

    With this, you should be able to say:

    sudo /usr/local/sbin/bluenet start
    

    and then talk to a modem on /dev/rfcomm0. Try it with a terminal program:

    minicom -D /dev/rfcomm0
    

    In minicom, type AT and return, and try again if it doesn't work on the first attempt[1]; the phone should respond with OK. If it does, you're essentially in business. If it doesn't, try rfcomm -a – which should show something like:

    rfcomm0: DE:VI:CE:MA:CA:DD:RE channel 1 clean
    

    Oh, and bluetootctl may be your (slightly twisted) friend; in particular info <mac> has helped me figure out problems. I still don't understand why my N900 doesn't show the rfcomm capability in there, though – again, I'm not nearly enough of a bluetooth buff to tell if that's normal in any way.

    The PPP part

    Assuming, on the other hand, you have your rfcomm connection, the rest is standard pppd fare (which, of course, makes me feel young again). This means you have to give a provider-specific pppd configuration, conventionally in /etc/ppp/peers/bluez (replace bluez with whatever you like), which for me looks like this:

    /dev/rfcomm0
    115200
    debug
    noauth
    usepeerdns
    receive-all
    ipcp-accept-remote
    ipcp-accept-local
    local
    nocrtscts
    defaultroute
    noipdefault
    noipv6
    connect "/usr/sbin/chat -v -f /etc/ppp/chat-bluez"
    
    lcp-echo-interval 300
    lcp-echo-failure 10
    

    Some of this is a good idea whenever there's not actually a serial port in the game (local, noctsrts), some may break your setup (noauth, though I think that's fairly normal today), and the lcp-echo things I found useful to detect lost connections, which of course are rather common on cellular data. Oh, and then there's the noipv6 that you may object to.

    Anyway: you may need to gently adapt your pppd peers file. Use your common sense and browse the pppd man page if you can't help it.

    The chat script /etc/ppp/chat-bluez mentioned in the peers file for me (who's using a German E-Netz reseller) looks like this:

    TIMEOUT 5
    ECHO ON
    ABORT 'ERROR'
    ABORT 'NO ANSWER'
    ABORT 'NO CARRIER'
    ABORT 'NO DIALTONE'
    '' "ATZ"
    OK-ATZ-OK ATE1
    OK 'AT+CGDCONT=1,"IP","internet.eplus.de","0.0.0.0"'
    TIMEOUT 15
    OK "ATD*99***1#"
    CONNECT ""
    

    Essentially I'm doing a modem reset (ATZ), and (to accomodate for the swallowed initial characters mentioned above) I'm trying again if the first attempt failed. The ATE1 enables echo mode to help debugging, and then comes the magic in the CGDCONT (“Define packet data protocol (PDP) context”) AT command – you will have to adapt at least the string internet.eplus.de to your provider's APN (there are public lists of those). The rest you can probably keep as-is; nobody really uses more than one profile (the 1) or a PDP type other than IP (well, there IPV4V6 that might interest you if you've removed the noipv6 statement above), or uses the PDP address in the last argument.

    The final dial command (the ATD) is, I think, fairly standard (it says, essentially: Call the PDP context 1 set up in CGDCONT).

    All this assumes your provider does not require authentication; I think most don't these days. If they do, say hello to /etc/ppp/pap-secrets and the pppd manpage (you have my sympathy).

    Finally, the various components are assembled in a stanza in /etc/network/interfaces:

    iface n900 inet ppp
      pre-up /usr/local/sbin/bluenet start
      provider bluez
      post-down /usr/local/sbin/bluenet stop
    

    That's it – your tethered network should now come up with ifup n900, and you can take it down with ifdown n900. If the thing gets stuck, meaning the interface won't come up as far as Debian is concerned, the post-down action will not be run. Just run bluenet stop manually in that case.

    Amazing endurance

    One thing blew my mind when I did this: A good decade ago, the Nokia N900 didn't come with the profile rfcomm uses enabled (it's called “DUN” there; don't ask me why). It didn't need much to enable it, but you needed to drop some magic file for the upstart init system running in the N900's maemo into some strategic location, and some kind soul had packaged that up. Not expecting much I simply ran apt-get install bluetooth-dun – and it just worked. Whoever still keeps up these ancient maemo repositories: Thanks a lot, and I'm impressed. If we ever meet, remind be to buy you a $BEVERAGE.

    [1]In the communication with the N900 with its ancient bluetooth stack, for some reason the first character(s) tend to get swallowed somewhere on the way. I've not tried to ascertain who is to blame; perhaps it's the autoconnection of rfcomm?
  • Ach, Sparda!

    Werbeschriften der Sparda-Bank

    Bin ich komisch, weil ich nicht gerne Geld geboten bekomme fürs Ausliefern von Bekannten an Banken?

    Ich bin leider Kunde der Sparda-Bank Baden-Württemberg. „Leider“ beispielsweise, weil mich die Bank dann und wann mit 75 Euro dazu bringen will, als Makler für sie tätig zu werden. Fände ich das Konzept Ehre nicht kreuzdoof, müsste ich wegen Betrag und Inhalt des Angebots Satisfaktion fordern.

    Lästiger im Alltag ist das Online-Banking, zumal es mit jedem „Relaunch“ schlimmer wird. Die utopische Hoffnung, ich könnte Kontoauszüge per PGP-verschlüsselter Mail bekommen und Überweisungen per signierter Mail in einem einfachen, maschinenlesbaren Format einreichen, habe ich dabei schon längst aufgegeben. Wahrscheinlich zwingen die diversen Zahlungsdiensterichtlinien die Bank wirklich zu (aus Sicht EDV-kompetenter Menschen) unsinnigen Schnittstellen.

    Aber wenn es schon hakelige Webseiten sein müssen, sollten sie wenigstens technisch so halbwegs in Ordnung sein. Fantastische Maximalforderung: die elementaren Funktionen sollten ohne Javascript gehen; das würde viel mehr für die Sicherheit tun als alle Zweifaktorauthentifizierungen des Universums. I have a dream.

    Die Sparda jedoch geht, wie gesagt, mit großen Schritten in die entgegengesetzte Richtung, kürzlich zu einem Laden namens TEO, mit dem sich einige Sparda-Genossenschaften, wie es aussieht, eine Art Fintech leisten wollten. Um damit nicht gleich bauchzulanden, ziehen sie ihre Kunden aus ihren alten (eingestandenermaßen auch unangenehmen) Systemen dorthin um.

    Schon, dass die KundInnen beim Umzug ein manuelles „onboarding“ samt Neueinrichtung der Konten sowie der (Nicht-) Zustellung von Kontoauszügen duchlaufen müssen, wirkt unnötig umständlich, so sehr ich verstehen kann, dass TEO nicht unbedingt die (vermutlich eher bescheidenen) Passwort-Hashes des alten Sparda-Eigenbaus übernehmen wollten. Bei den Investitionssummen, um die es hier vermutlich geht, wären jedoch elegantere Migrationspfade („bitte ändern sie jetzt ihr Passwort“) schon denkbar gewesen.

    Das um so mehr, als das onboarding richtig schlecht gemacht ist: Es erscheint erstmal ein leerer Bildschirm, und das ändert sich auch dann nicht, wenn mensch den Browser Javascript ausführen lässt. Kein Fallback, nichts, immer nur ein weißes Fenster. Das Javascript von TEO nämlich wirft eine Exception, wenn es kein local storage bekommt, und niemand fängt diese Exception. Ganz im Stil der neuen Zeit gibt es nicht mal ein minimales HTML ohne Skripting – nur weiß. Au weia.

    Zwei Zeilen Javascript würden reichen, um nicht verfügbare local storage zu diagnostizieren und das etwas weniger murksig zu machen (ordentliches HTML wäre natürlich immer noch nett). Etwas mehr Arbeit ist es, dann zu erklären, was da wie lang gespeichert wird und warum es ohne nicht gehen soll. Das jedoch würde ohnehin in die Datenschutzerklärung gehören, viel zusätzliche Arbeit wäre es also nicht. Wenn die Datenschutzerklärung etwas taugen würde, heißt das. Aber dazu gleich.

    Ich könnte jetzt weitermachen mit Punkten, an denen der Kram an allen möglichen und unmöglichen Stellen kaputt geht, ohne dass es irgendeine Sorte Fehlermeldung außerhalb der Javascript-Konsole (und welche Muggels kennen die schon?) gibt. Aber selbst wenn mensch den Browser – was bei Bank-Geschäften schon als schlechte Idee gelten kann – nach und nach immer unvorsichtiger konfiguriert, sind noch schwer verstehbare Schnitzer im Programm, etwa, dass der Flickercode die falsche Größe hat. Die meisten Clients haben inzwischen eine recht zuverlässige Idee von der physikalischen Größe der Anzeige; ein width: 6.5cm im CSS ist doch wirklich nicht zu viel verlangt.

    Ähnliche WTF-Momente ergeben sich in der Datenschutzerklärung der TEOs. Derzeit heißt es dort in 2.3:

    Zur Erhöhung der Kundensicherheit nutzen wir in TEO den Service reCAPTCHA der Google Ireland Limited, Gordon House, 4 Barrow St, Dublin, D04 E5W5, Irland. Im Rahmen der Registrierung in TEO wird das reCAPTCHA zur Erkennung und Unterscheidung zwischen missbräuchlicher Nutzung personenbezogener Daten durch Maschinen und menschliche Eingaben eingesetzt.

    Als ich das gelesen habe, habe ich erstmal mit dem „onboarding“ aufgehört, denn wer mir was verkaufen will, sollte nicht verlangen, dass ich Schaufenster oder Verkehrsschlider auf Zuruf von google abklicke, und mich schon gar nicht zum Ausführen von googles Code nötigen (ebay, hörst du mich?). Sparda beruft sich für diese Frechheit auf Art. 6 1(f) DSGVO, also berechtigte Interessen des Betreibers. Das ist offensichtlich Quatsch; Textchas oder selbst hostbare Tests funktionieren besser und unter weniger Kundenverprellung.

    Am Ende musste ich aber dringend überweisen und war entschlossen, den reCaptcha-Mist zur Not doch durchzustehen. Musste ich am Ende nicht nicht, google hielt meinen Browser offenbar wirklich für menschengesteuert – wer weiß, warum. Aber TEO lädt tatsächlich eine Datei recaptcha__de.js von google-Servern, und zwar dort aus einem Pfad wie /recaptcha/releases/CdDdhZfPbLLrfYLBdThNS0-Y/recaptcha__de.js – da könnte ich schon allein bei der URL ins Grübeln kommen, was für Informationen google da wohl aus dem Banking ausleiten mag (ok: ja, ich vermute in Wirklichkeit auch, dass die Buchstabensuppe eher etwas wie einen git-Commit auf google-Seite identifiziert – aber wenn die googles wollten, könnten sie über Pfade dieser Art fast beliebige Mengen an Information exfiltrieren, was erst dann auffallen würde, wenn mal wer Pfade verschiedener Clients vergliche).

    Was denken sich Leute, wenn sie ohne Not reCaptcha auf ihre Seiten packen? Diese Frage stellt sich fast noch mehr beim nächsten Punkt:

    2.4 Google Web Fonts

    Um Inhalte browserübergreifend korrekt und grafisch ansprechend darzustellen, werden in TEO Web Schriften von Google Fonts verwendet. Die Einbindung dieser Web Fonts erfolgt nicht über Google. Die Schriften liegen lokal auf Servern. Es erfolgt kein externer Serveraufruf bei Google. Es findet keine Informationsübermittlung an Google statt.

    Wenn ich recht verstehe, was die da sagen wollen, dann wäre das: Wir nehmen Fonts von google, aber ziehen sie nicht von google. Dann stellt sich die Frage, was diese Information hier soll – wenn die anfangen, aufzuzählen, wer alles irgendwie an der Software beteiligt war, die auf ihren Servern läuft, sind sie lang beschäftigt.

    Um das noch absurder zu machen: sie lügen (jedenfalls im Effekt). Zumindest gestern und heute versucht die Seite für mich, die URL https://fonts.gstatic.com/s/roboto/v18/KFOmCnqEu92Fr1Mu4mxP.ttf (und noch zwei weitere; ich hoffe, die Buchstabensuppe erhält nicht allzu viel Kompromittierendes über mich) zu dereferenzieren. Damit hat google die IP, via Referrer den Umstand, dass gerade wer überweist, und vermutlich auch die Muggel-Identität, da mich wundern würde, wenn nicht normal der google-Cookie auch an gstatic gehen würde (ich muss gestehen, das nicht näher untersucht zu haben, zumal fonts.gstatic.com auf meinen Maschinen zu localhost auflöst).

    Was soll das? Wenn TEO schon Geld in die Hand nimmt und meint, mir die Wahl des Fonts vorschreiben zu müssen (was ich übrigens lästig finde), dann sollen sie wenigstens irgendwas kaufen statt die vergifteten Geschenke von google zu nehmen und dann noch in ihrer Datenschutzerklärung zu lügen.

    Für eine teilweise Erklärung für die, na ja, Lüge (ich will gnädig sein: „das Versehen“) mit den google-Fonts hilft ein Blick auf die 107 (!) Requests, die mein Browser macht, um die Wurzelseite des Online-Bankings aufzubauen. Dabei zeigt sich, dass immerhin die meisten Daten von TEO-Servern kommen. Es bleiben aber, nach der Vorrede kaum überraschend, doch einige Cross-Site-Requests.

    • die erwähnten Google-Fonts
    • ein CSS, ein Logo, das Captcha-Javascript (von www.gstatic.com)
    • eine bizarre Datei „anchor“, die offenbar im Zusammenhang mit Captcha steht (und die den Browser vermutlich dazu bringt, die Fonts von fonts.gstatic.com zu ziehen)
    • Und dann noch Ressourcen list und logoutWeb von prod-teo.itmr.de, beides JSON, ersteres mit Zeug, was erstmal nach Werbequatsch aussieht, aber vermutlich Identitäsinformation ist.

    Die Suche nach ITMR in der Datenschutzerklärung ist vergeblich, www.itmr.de zeigt ohne Javascript nur einen Spinner:

    Ein Warte-Spinner

    Wenn dieses Ding genug Schöpfungshöhe für Copyright hat, liegt dieses vermutlich bei itmr. Hier verwendet unter der, klar, Satireklausel.

    Der Spinner ist schon mal deutlich mehr als die leere Seite von TEO. Mit Javascript kommt sogar ein Titel: „ITM Research GmbH – Smart Cloud Computing”, ansonsten bleibt es beim Spinner. ITM Research… nichts, was Wikipedia kennen würde. Smart? Na ja. Zieht auch erstmal fünf Pfund Zeug von Google, Fonts und eine Familienpackung Javascript von maps.googleapis.com, einen Haufen PNGs von www.google.com. Ziemlich viel Kram für einen rotierenden Spinner.

    Wenn ich der Seite obendrauf local storage erlaube, kommt endlich das heiß erwartete Cookie-Banner, und zwar so:

    Cookie-Banner mit vor-abgeklickten Trackingcookies

    Echt jetzt? Per default angeklickte Tracking-Cookies? Wie smart muss mensch wohl sein, um für so ein bisschen Überwachungsmehrwert ein fettes Bußgeld zu riskieren?

    Zurück zur Ausgangsfrage: Was für Daten tauscht die Sparda eigentlich mit diesen etwas anrüchigen Gesellen aus? „ITM-ation – unser Synonym für digitale Transformation und Innovation“ schreiben sie, wenn ihnen der Browser gut genug ist. Diese Leute bekommen mit, wenn ich mit der Sparda-Bank rede? Uh.

    Ich klicke auf „Mehr Erfahren”. Beim Bullshit Bingo hätte ich gleich gewonnen:

    Wir realisieren Projekte der digitalen Transformation und Innovation, um Ihnen durch die Digitalisierung von Geschäftsmodellen, Wertschöpfungsketten und Prozessen mehr Wachstum in einem immer stärkeren Wettbewerbsumfeld zu ermöglichen. [...] Wir entwickeln Apps und Web-Apps, um Geschäftsprozesse in digitale Ökosysteme zu transferieren. Wir entwickeln dabei Ihre Datenveredelungsstrategie, setzen aber auch externe Echtzeit-Dienste der künstlichen Intelligenz (KI) wie zum Beispiel Cognitive Services und Watson ein. [...] Eine positive User Experience ist unabdingbar in der Digitalisierung Ihrer Prozesse. Um das jederzeit und von überall zu erzielen, brauchen Sie zwischen Ihrer Anwendung und Ihren Kunden über alle Kanäle und Anwendungsoptionen hinweg funktionale und ansprechende Benutzeroberflächen und Schnittstellen.

    Der Bullshit gegen Ende suggeriert (ebenso wie der konsistente Totalausfall ohne …

  • udev, thinkpad docks, sawfish

    The other day someone gave me another dock for my thinkpad, and I eventually decided to use it at home. I've had a dock at the office for a long time, and docking there involved running a complex script configuring the network environment, running a window manager on some display on a desktop machine, and only exiting when the dock was supposed to end; its execution was triggered when the wake-up script noticed a dock was present.

    Now, when there are two docks and one is for rather conventional at-home use (essentially, simply configuring a different monitor and network adapter), I decided to do it properly and go through udev. Which turned out to be tricky enough that I'll turn this note to my future self into a blog post.

    udev

    What turned out to be the most complicated part was figuring out the udev rules. That's because for ages I have been using:

    udevadm info -a -p some/sysfs/path
    

    to work out matchable attributes for a device. That's more or less fine if all you're after is rules for attaching devices. For the dock, however, the removal event is important, too. But when the removal event comes in, udev has forgotten essentially all of the attributes that come from info -a, and rules that work with add simply won't fire with remove.

    So, here's my new policy: I'll use:

    udevadm monitor --environment --udev
    

    (where the udev option restricts events to udev rather than kernel events, which for the deluge of events coming from the dock seemed smart; I may want to revisit that). When you then plug in or out things, you'll directly see what events you can match against. Nice.

    Except of course for the deluge of events just mentioned: A dock just has quite a few devices. The event for the device I consider most characteristic, however, makes two add events, and I've not found a good way to tell the two of them apart. Still, this is what I've put into /etc/udev/rules.d/95-docking.rules:

    ACTION=="add", SUBSYSTEM=="usb", ENV{ID_VENDOR_ID}=="17ef", \
      ENV{ID_MODEL_ID}=="1010",  ENV{DEVTYPE}=="usb_device", \
      RUN+="/bin/su <your user id> -c '/full-path-to-dock-script start'"
    
    ACTION=="remove",  ENV{SUBSYSTEM}=="usb", ENV{PRODUCT}=="17ef/1010/5040", \
      ENV{DEVTYPE}=="usb_device", \
      RUN+="/bin/su <your user id> -c '/full-path-to-dock-script stop'"
    

    Important (and having forgotten about it again gave me quite a bit of frustration): Rather sensibly, udev has no idea of the shell path and will just fail silently when it cannot execute what's in RUN. Hence you must (essentially) always give full path names in udev RUN actions. In case of doubt, try RUN+="/usr/bin/logger 'rule fires'" in a rule and watch the syslog.

    For this kind of thing, by the way, you'll rather certainly want to use su (or go through policykit, but I can't bring mayself to like it). You see, I want the dock script in my home directory and owned by me; having such a thing be executed as root (which udev does) would be a nice backdoor for emergencies, but will generally count as a bad idea.

    On the double dock event… well, we're dealing with shell scripts here, so we'll hack around it.

    Dock script: sawfish to the rescue

    udev only lets you execute short scripts these days and rigorously kills everything spawned from udev rules when it has finished processing the events. I suppose that's a good policy for general system stability and reducing unpleasant surprises. But for little hacks like the one I'm after here, it seems to be a bit of a pain at first.

    What it means in practice is that you need something else to execute the actual dock script. In my case, that thing is my window manager, sawfish, and having the window manager do this is rather satisfying, which reinforces my positive feeling towards udev's kill policy (although, truth be told, the actual implemenation is in shell rather than in sawfish's scheme).

    To keep everything nicely together, the docking script at its core is a bash case statement, in essence:

    !/bin/bash
    # bookkeeping: we need to undock if that file is present
    UNDOCK_FILE=~/.do-undock
    
    # display for the window manager we talk to
    export DISPLAY=:0
    
    case $1 in
      start)
        sawfish-client -c "(system \"urxvt -geometry -0+0 -e $0 on &\")"
        ;;
      stop)
        sawfish-client -c "(system \"urxvt -geometry -0+0 -e $0 off &\")"
        ;;
      on)
        if [[ -f $UNDOCK_FILE &&
          $((`date +"%s"` - `date -r $UNDOCK_FILE +"%s"`)) -lt 20 ]]; then
            # debounce multiple dock requests
           exit 1
        fi
        touch $UNDOCK_FILE
    
        # let udev do its thing first; we're no longer running from udev
        # when we're here.
        udevadm settle
    
        # Commands to dock follow here
        ;;
      off)
        if [ -f ~/.do-undock ]; then
          rm ~/.do-undock
          # Commands to undock in here.
        fi
        ;;
      *)
        echo "Usage $0 (start|stop|on|off)"
        ;;
    esac
    

    The plan is: Udev calls the script with start and stop, which then arranges for sawfish to call the script from sawfish with the on and off arguments.

    There's a bit of bookkeeping with a file in home I keep to see whether we need to undock (in my setup, that's not necessary at work), and which I use to unbounce the duplicate dock request from udev. That part could be improved by using lockfile(1), because the way it is written right now there are race conditions (between the -f, the date, and the touch) – perhaps I'll do it when next I have time budgeted for OS fiddling.

    One think I like a lot is the udevadm settle; this basically lets my script rely on a defined state where all the devices it may want to talk to are guaranteed to be initialised as far as udev goes. This is so much nicer than that sleep 3 you can see in too many places.

    What I do when docking

    Why go into all this trouble and not let whatever automagic is active pick up the screen and the new network interface and be done with it? Well, partly because I don't run most of the software that does that magic. But of course having a shell script lets me say what I actually want:

    • disable sleep on lid closing (that's special to my own ACPI hacks from the depths of time)
    • configure the the external screen as primary (that's something like xrandr --output DP2-1 --off ; xrandr --fb 2048x1152 --output DP2-1 --auto for me; don't ask why I first need to switch off the display, but without it the --auto will get confused).
    • switch to an empty (“dock-only” if you will) page on the desktop (that's wmctrl -o 4096,1152 for me).
    • sure enough, switch on some desktop glitz that I'm too stingy for when off the grid.

    One thing I'm currently doing in the dock script that I shouldn't be doing there: I'm now using a wacom bamboo pad I've inherited as a mouse replacement at home and was suprised that no middle mouse button (Button2) was configured automatically on it. Perhaps some search engine will pick this up and save a poor soul looking for a quick solution reading man pages and xsetwacom output:

    xsetwacom set "Wacom BambooPT 2FG 4x5 Pad pad" Button 8 2
    xsetwacom set "Wacom BambooPT 2FG 4x5 Pad pad" Button 9 2
    

    (this makes both buttons in the middle middle mouse buttons; I'll see if I like that on the long run). Of course, in the end, these lines belong into a udev rule for the wacom tablet rather than a dock script. See above on these.

  • Upgraded to bullseye

    I've upgraded my personal notebook – with a file system that has a continuous history back to a slackware in 1996 and thus always is the most rewarding thing to test upgrades on – to Debian bullseye today.

    It's been a while since the last dist-upgrade messed up my X or rendered a machine unbootable, but they still never fail to be exciting. This one was no exception.

    logind and systemd

    The one major trouble as far as Debian proper is concerned was that the full-upgrade pulled in systemd again (which I still can't get myself to adopt on boxes I fiddle with a lot). This was because at some point I had purged elogind (which doesn't do anything on this box), and both a few KDE programs I have installed and the indispensable gparted need policykit-1, which in turn pulls in some logind; if you don't have one, apt will migrate you to systemd.

    Well, it was easy to go back this time: Just say:

    apt install elogind
    apt install sysvinit-core
    

    and all is back to normal again with the wonderful shell script goo during system startup. Hooray for elogind! I will admit I've not quite figured out what its actual use is, though. But that's probably because I've never quite figured out policykit, which in turn is probably because I think there's nothing wrong with sudo. But, as you'll see in a second, I might be swayed.

    Sure enough: Power

    I'm running all kinds of interesting scripts when the machine goes through various power states; for instance, I'm kill -STOP-ing a few pieces of moving eye candy when the box loses grid power, and I'm kill -CONT-ing them when the power is back. This happens through shell scripts I've dropped into /etc/pm/power.d, from where pm-utils has nicely been executing them for the last 10 years or so.

    Alas, they didn't run any more after the upgrade. Instead, when I shut the lid, the box would sleep right again after waking up. This last thing was fixed quickly: Just tell elogind not to bother in /etc/elogind/logind.conf.

    That the pre-sleep and post-wakeup scripts still ran soothed my first worry – that pm-utils might have had an RC- (release critical) bug and dropped out of Debian. Going through pm-utils' /usr/share/doc info made me worry again, though: the last upstream change there is from 2010, and the last Debian changelog entry is from 2019, mentioning an open RC bug. Uh-oh. It seems I might soon need to try harder with elogind.

    But not just yet, as the trace to work this out was bug #772275 (oh yes, the bug page for pm-utils makes we worry, too): pm-utils used to receive the AC/Battery notification from acpi-support, and that clearly broke in some way. At least for me, and with this upgrade. Poking around a bit in /etc/apci didn't show an immediate hook; yes, there's power.sh, but that gets called a lot on my box if the moon is right (for Lenovo's crappy firmware at least), and one would need to figure out whether or not there's grid power oneself.

    So, I dug a bit deeper and noticed that ever since I've moved from laptop-mode-tools to tlp, pm-utils were almost obsolete because tlp actually does everything it does all without pm-utils – but it doesn't let me run my beloved shell scripts (“by design“, its authors say). Hence, it's not byebye to pm-utils yet.

    But I like the way that tlp uses to be notified of power events: through udev. Taking that thought a bit further so I don't have to do any manual state management (as pm-utils doesn't have the equivalent of tlp auto) and filter out power events for batteries (which I don't care about), I ended up introducing two new udev rules that look relatively generic to me:

    ACTION=="change", SUBSYSTEM=="power_supply", ATTR{type}=="Mains",\
      ATTR{online}=="1", RUN+="/usr/sbin/pm-powersave false"
    ACTION=="change", SUBSYSTEM=="power_supply", ATTR{type}=="Mains",\
      ATTR{online}=="0", RUN+="/usr/sbin/pm-powersave true"
    

    Drop this into /etc/udev/rules.d/10local.rules (or so), and pm-utils' power.d works again.

    Another python2 grace time

    But the real elephant in the room is that bullseye in effect drops Python version 2. While this certainly does not come as a surprise, it still hurts me a lot, because I have plenty of self-written larger or smaller python2 programs – my audiobook-reader, my local wikipedia, my work time accounting and a gazillion little other things. And there's things like editmoin that haven't been ported yet either.

    Well, I had hoped I could keep the buster python2 packages around, perhaps even using the python-is-python2 package. But really, I don't think that's an option for a halfway lively system (which will use quite a few python3 packages). I gave up on that idea more or less as soon as I realised that the python-docutils-common dependency (and docutils I need left and right) will conflict between the docutils from buster and from bullseye. Trying to keep buster packages will clearly become incredibly fiddly.

    So, instead I figured I ought to keep the legacy software alive while finally porting it as I go along (one, my one-line CLI, I actually have ported this morning) using a python2 “virtual” (yeah, right, virtual...) environment.

    Yes, virtual environments are evil all around, not only because their content rots without anyone noticing; but then this is exactly about letting things rot in a halfway controlled fashion, so I claim this is a use case.

    In case others mourn the demise of python2 in bullseye and want to go slowly when migrating, here's what to do:

    1. Make sure the python2 packages that still are in bullseye are in place. This would be python2.7, python2.7-dev, and presumably python-tk. Of course, you will want the virtualenv package, but that's already python3.

    2. Create the virtual environment:

      virtualenv -p python2.7 ~/.legacy-python
      
    3. Make it simple to use that. For that, add:

      alias enable-oldpython='export PATH=~/.legacy-python/bin:$PATH'
      

      to your .aliases (or whereever else you keep your aliases) and exec bash in the current shell to try that out. This is when you want want to run pip, or at any other time when you want your python to be what's in the virtual environment.

      But this won't work for hashbangs. To make that work, put a file like:

      #!/bin/sh
      export PATH=/home/<YOUR USERNAME>/.legacy-python/bin/:$PATH
      exec python "$@"
      

      somewhere into your path as, say, oldpython. Since I still have some system-wide things using python2, I ended up sticking this into /usr/local/bin. Given python2 has been out of security support for more than a year now, I might be regretting that; on the other hand, python's core hasn't had many security problems in the past 20 years, and so I figure I am fine. Caveat emptor, though.

    4. Then, run pip install and/or python setup.py install to your heart's delight. All this isn't forever, so this one time I found myself capable of forgetting the long run, later upgrades, and all that. Just remember: sudo and pip never mix, and they particularly badly mix here. Oh: I actually found myself apt-get source-ing python packages from buster and just running python setup.py install in them because in my book that's a good deal less obscure than pip.

    Limping along with a private MoinMoin

    But then came the bitter realisation: There's no moinmoin in bullseye any more. That's a killer for at least three servers I'm operating. And, really, looking at what the MoinMoin folks write on python3 (in particular at its list of dependencies), I shudder in the expectation of seeing something quite in line with my unpleasant experiences with mailman2 happen with MoinMoin.

    On my box, however, I can live with an aging service (that only listens to localhost), and I can live with having moinmoin be a CGI. In case these considerations (typically, for a “notes and observations”-style wiki) apply to you as well, here's what I did to make the legacy moinmoin run in my bullseye apache2.

    First, I installed moinmoin into the “virtual” python 2.7 I created above:

    enable-oldpython  # the alias above
    pip install moin  # no sudo!
    

    Then I fixed the apache configuration to use that. So, I commented out the previous MoinMoin integration and replaced it with something like:

    <Directory /home/<YOUR USER NAME>/.legacy-python/share/moin/server>
      AllowOverride None
      Options +ExecCGI -MultiViews +SymLinksIfOwnerMatch
      Require all granted
    </Directory>
    <Directory /home/<YOUR USER NAME>/.legacy-python/lib/python2.7/site-packages/MoinMoin/web/static/htdocs>
      AllowOverride None
      Require all granted
    </Directory>
    
    ScriptAlias /meiner /home/<YOUR USER NAME>/.legacy-python/share/moin/server/moin.cgi
    Alias /wiki/ /home/<YOUR USER NAME>/.legacy-python/lib/python2.7/site-packages/MoinMoin/web/static/htdocs
    Alias /moin_static1911 /home/<YOUR USER NAME>/.legacy-python/lib/python2.7/site-packages/MoinMoin/web/static/htdocs
    

    (you may need …

  • Fürchten lernen 3: Microsoft

    Nach dem zweiten Teil meiner Leidensgeschichte über den Betrieb eines Mailservers in der Postmoderne ist es ruhig geworden: Nachdem ich erstmal das DNS vertrauenswürdig gestaltet hatte, fanden eigentlich alle, bei denen mein Server Mails einliefern wollte, dessen Reputation reiche schon hin.

    Ich hegte also Hoffnung, dass das alles nicht so schlimm ist wie ich geglaubt hatte, dass SMTP noch nicht kaputt ist und mensch mit vernünftigem Aufwand selbst Mail verteilen kann. Bis heute morgen, als von meinen Mailserver das hier zurückkam:

    XXXXXXXXXXXXX@XXXXXXXXXX.de
       host XXXXXXXXXXXX01i.mail.protection.outlook.com [104.47.2.36]
       SMTP error from remote mail server after RCPT TO:<XXXXXXXXXXXXX@XXXXXXXXXX.de>:
       550 5.7.606 Access denied, banned sending IP [116.203.206.117]. To request
     removal from this list please visit https://sender.office.com/ and follow the
     directions. For more information please go to
     http://go.microsoft.com/fwlink/?LinkID=526655 AS(1430)
    

    Oh nein. Microsoft. Na, mal sehen.

    Ich schicke meinen Web-Browser zu sender.office.com, und es erscheint einen Dialog mit einem kreiselnden Wartedings, ein sicheres Zeichen, dass da mal wieder „Web-Programmier“ am Werk waren, die einfach so voraussetzen, dass sie Code auf allen Maschinen ausführen dürfen. Na super: Mails verschicken geht nicht, ohne Microsoft-Code laufen zu lassen.

    Aber egal, sollen sie halt auf meine CPU; das Javascript zieht ein Captcha ein (zum ersten Mal bin ich froh, dass das Microsoft ist, denn so ist es immerhin kein Google-Captcha und ich muss nicht deren „KI“ trainieren). Captcha gelöst, für ein paar Sekunden passiert nichts, dann eine Meldung:

    Step 1: Our messaging service has experienced a temporary issue,
    please resubmit your information below.
    

    Mach ich das mit dem resubmit, gleiche Reaktion. Hatte ich was anderes erwartet?

    Ok, kann ja sein, dass die einfach keine gute Fehlermeldung haben und das irgendsoein dämlicher CSRF-Schutz mit Referrern ist (auch wenn mir wirklich nicht klar wäre, wer auf diesem Zeug CSRF machen wollen könnte). Lasse ich meinen Browser also noch Referrer-Header schicken. Keine Änderung, immer noch kaputt.

    An der Stelle werde ich sauer und will irgendwem eine zornige Mail schicken. Aber: Keine Kontaktadresse, nirgends. Ich überlege, ob ich eine Telemediengesetz-Beschwerde auf den Weg schicken soll, denn zweifelsohne ist das ein kommerzielles Angebot unter deutschem Recht (der Empfänger ist eine hiesige Firma, für die Microsoft das bestimmt nicht umsonst macht). Vielleicht. Aber bevor ich mit einem Scheiß wie dem Telemediengesetz argumentiere, brauche ich noch mehr Verzweiflung.

    Aber gucken wir erstmal, was da „more information“ ist. Ich lasse meinen Browser also auf http://go.microsoft.com/fwlink/?LinkID=526655 los und bekomme ein Redirect auf:

    https://technet.microsoft.com/library/mt661881(v=exchg.150).aspx
    

    Sowohl auf luakit als auch per curl reagiert der Microsoft-Server darauf mit:

    Unable to process request
    

    Gratuliere. Was machen diese Leute eigentlich beruflich?

    Aber wurst, werfe ich halt meinen Browser für wüste, grob privatsphäreverletzende Seiten an – mein Leben ist zu kurz, um den Quatsch, den die Firma da auf die Menschheit loslässt, verstehen zu wollen. Mit dem Browser fürs Grobe kommt dann auch was: eine Seite mit viel Rede von „Defender”. Aber keine nützliche Information, und so probiere ich halt sender.office.com nochmal mit dem Kamikaze-Browser.

    Ergebnis: Our messaging service has experienced a temporary issue, please resubmit your information below.

    Immerhin hat ist auf der Info-Seite etwas, das nach einem Kontakt-Link aussieht. Aber nein, es ist eine Issue-Seite auf github, https://github.com/MicrosoftDocs/feedback/issues, 1630 Open Bugs. Jaklar, da schreibe ich meinen noch dazu. Was glauben diese Leute eigentlich?

    Stattdessen habe ich dann heute vormittag an postmaster@office.com geschrieben, und Mail ist nicht gleich zurückgewiesen worden. Entsprechend hatte ich da noch Hoffnung, Microsoft könnte sich immerhin an diesen Teil der Mail-RFCs halten. Jetzt, 18 Uhr, sieht es nicht danach aus, da kam genau keine Reaktion (im diesem Vergleich, es schmerzt mich, das zuzuzugeben, sieht die Telekom viel besser aus). Und die sender.office.com-Seite ist auch noch kaputt.

    Andererseits: Offensichtlich haben die Exchange-Leute gerade tatsächlich außergewöhnliche Probleme. Vielleicht habe ich einfach nur Pech gehabt und das ist sonst nicht so rekursiver Murks?

    Nachtrag (2021-03-12)

    (um 15:30) Immerhin weist Microsoft Mails an postmaster nicht so rüde ab wie andere Mails. Mein Mailserver schreibt mir gerade, dass er es 24 Stunden lang nicht geschafft hat, die Mail an postmaster@office.com auszuliefern, dass er es aber weiter probieren wird. Es bleibt spannend. Unterdessen findet aber yahoo.de, dass es mit meiner Reputation bergab geht. Oh je. Das sieht nach einem Alptraum mit Verzögerung aus.

    Nachtrag (2021-03-15)

    (mittags) Nee, natürlich ist nichts bei postmaster@office.com einzuliefern. Wo kämen wir da auch hin. Dafür macht inzwischen wenigstens die sender.office.com-Geschichte etwas, wenn auch von der Mail, die das verspricht, innerhalb von 10 Minuten nichts zu sehen ist.

    Nachtrag (2021-03-15)

    (abends) Nach noch einem Versuch mit der sender.office.com-Geschichte kam dann auch eine Mail mit einem Link, und dessen Derefenenzierung hat tatsächlich etwas produziert, das versprach, mein Server werde innerhalb von 30 Minuten von der Blacklist genommen. Schon frech, wie dieser Laden über die Zeit anderer Menschen verfügt. Auf der anderern Seite: jetzt will ich da gar niemandem mehr Mails schreiben. Pfft.

  • Mailman3: "Cannot connect to SMTP server localhost on port 25"

    I've been a fairly happy mailman user for about 20 years, and I ran mailman installations for about a decade in the 2000s.

    Over the last week or so, I've spent more time setting up a mailman3 list off and on than I've spent with mailman guts in all the years before, which includes recovery form one or two bad spam attacks. Welcome to the brave new world of frameworks and microservices.

    Perhaps the following words of warning can help other mailman3 deployers to not waste quite as much time.

    Badly Misleading Error Messages

    Most importantly, whatever you do, never call mailman as root. This will mess up permissions and lead to situations really hard to debug. In particular, the error message from the post's title:

    Cannot connect to SMTP server localhost on port 25
    

    apparently can have many reasons (or so the recipes you find on the net suggest), few of which have anything to do with SMTP, but one clearly is when mailman can't read or write to queue files or templates or whatever and bombs out while trying to submit mail.

    Morale: Don't claim too much when writing error messages in your programs.

    Unfortunately, I've fixed the thing accidentally, so I can't say what exactly broke. The take away still is that, in Debian (other installations' mailman users might be called something else) you run mailman like this:

    sudo -u list mailman
    

    However, I can now say how to go about debugging problems like these, at least when you can afford a bit of mailman unavailability. First, stop the mailman3 daemon, because you want to run the thing in the foreground. Then set a breakpoint in deliver.py by inserting, right after def deliver(mlist, msg, msgdata), something like:

    import pdb; pdb.set_trace()
    

    Assuming Debian packaging, you will find that file in /usr/lib/python3/dist-packages/mailman/mta.

    Of course, you'll now need to talk to the debugger, so you'll have to run mailman in the foreground. To do that, call (perhaps adapting the path):

    sudo -u list /usr/lib/mailman3/bin/master
    

    From somewhere else, send the mail that should make it to the mail server, and you'll be dropped into the python debugger, where you can step until where the thing actually fails. Don't forget to remove the PDB call again, as it will itself cause funky errors when it triggers in the daemonised mailman. Of course, apt reinstall mailman3 will restore the original source, too.

    Template Management Half-Broken

    When I overrode the welcome message for a mailing list, the subscription notifications to the subscribing users came out empty.

    This time, there was at least something halfway sensible in the log:

    requests.exceptions.HTTPError: 404 Client Error: Not Found for url: http://localhost/postorius/api/templates/list/kal.sofo-hd.de/list:user:notice:welcome
    

    Until you read up on the mailman3 system of managing templates (which, roughly, is: store URIs from where to pull them), it's a bit mystifying why mailman should even try this URI. Eventually, one can work out that when you configure these templates from Postorius, it will take the URI at which mailman should see it, Postorius, from POSTORIUS_TEMPLATE_BASE_URL in /etc/mailman/mailman-web.py. This is preconfigured to the localhost URI, which proabably only rarely is right.

    To fix it, change that setting to:

    POSTORIUS_TEMPLATE_BASE_URL = 'http://<your postorious vserver>/postorius/api/templates/'
    

    Of course it'll still not work because the old, wrong, URI is still in mailman's configuration. So, you'll have to go back to the template configuration in Postorius and at least re-save the template. Interestingly, that didn't seem to fix it for me for reasons I've not bothered to fathom. What worked was deleting the template and re-adding it. Sigh.

    As soon as you have more than one template, I expect it's faster to change the URIs directly in mailman's database, which isn't hard, as seen in the next section.

    [Incidentally: does anyone know what the dire warnings in the docs about not using sqlite3 on “production” systems actually are about?]

    Disable Emergency Moderation After Moving

    Basically because I was hoping to get a more controlled migration, I had set one list on the old server to emergency moderation before pulling the config.pck. Don't do that, because at least as of now mailman3 has the notion of emergency moderation but makes it hard to switch it on or off. I eventually resorted to directly touching mailman's config database (if you've configured mailman to use something else than sqlite, the shell command is different, but the query should be the same):

    $ sudo -u list sqlite3 /var/lib/mailman3/data/mailman.db
    [on the sqlite prompt:]
    update mailinglist set emergency=0 where list_id='<your list id>';
    

    Note that <your list id> has a dot instead of the at, so if your list is mylist@example.org, its id is mylist.example.org.

    Oh No, CSRF Token

    The list I cared about most could be joined from an external web site, transparently posting to mailman2's cgi-bin/mailman/subscribe (oh! CGI! How am I missing you in the age of uwsgi and Django!). Looking at its counterpart for modern mailman3, the first thing I noted is that there's a CSRF token in it – if you've not encountered them before, it's a couple of bytes the originating server puts into a web form to prevent what Postorius' authors feels is Cross Site Request Forgery.

    Of course, what I wanted was exactly that: Post to Postorius from a different web site. I don't feel that's forgery, very frankly.

    I didn't see an obvious way to turn it off, and I was a bit curious about mailman3's own http API, so I wrote a few lines of code to do this; the API part itself was straightforward enough, something like:

    result = requests.post(
      getConfig("mailmanAPI")+"/members", {
        'list_id': getConfig("mailmanListname"),
        'subscriber': toSubscribe,
        'pre_verified': False,
        'pre_confirmed': False,
        'pre_approved': True,},
      auth=(getConfig("mailmanAPIUser"),
        getConfig("mailmanAPIPassword")),
      timeout=1)
    

    – but of course it sucks a bit that subscribing someone requires the same privilege level as, say, creating a mailing list or changing its description. And all that just to work around CSRF prevention. Sigh.

    On top of that, I've tried K-SAT on the pre_X booleans to try and see if anything gives me the tried and tested workflow of “let folks enter a mail address, send a confirmation link there, subscribe them when it's being clicked“. No luck. Well, let's hope the pranksters don't hit this server until I figure out how to do this.


    Hm. I think I'm a bit too locked in into mailman to migrate away, but I have to say I wish someone would port mailman2 to python3 and thus let mailman2 hang on essentially forever. It did all a mailing list manager needs to do as far as I am concerned, and while it wasn't pretty with the default browser stylesheets, even now, almost a decade into mailman3, it works a whole lot more smoothly.

    Or perhaps there's a space for a new mailing list manager with a trivially deployable web interface not requiring two separate database connections? Perhaps such a thing exists already?

    Well, summing up, the central migration advice again: Mind the sudo option in

    sudo -u list mailman import21 my-list@example.org config.pck
    
  • A Mail Server on Debian

    After decades of (essentially) using other people's smarthosts to send mail, I have recently needed to run a full-blown, deliver-to-the-world mail server (cf. Das Fürchten lernen; it's in German, though).

    While I had expected this to be a major nightmare, it turns out it's not so bad at all. Therefore I thought I'd write up a little how-to-like thing – perhaps it will help someone to set up their own mail server. Which would be a good thing. Don't leave mail to the bigshots, it's too important for that.

    Preparation

    You'll want to at least skim the exim4 page on the Debian wiki as well as /usr/share/doc/exim4/README.Debian.gz on your box. Don't worry if any of that talks about things you've never heard about at this point and come back here.

    The most important thing to work out in advance is to get your DNS to look attractive to the various spam estimators; I didn't have that (mostly because I moved “secondary” domains first), which caused a lot of headache (that article again is in German).

    How do you look attractive? Well, in your DNS make sure the PTR for your IP is to mail.<your-domain>, and make sure mail.<your-domain> exists and resolves to that IP or a CNAME pointing there. Note that this means that you can forget about running a mail server on a dynamic IP. But then dynamic IPs are a pain anyway.

    Before doing anything else, wait until the TTL of any previous records of this kind has expired. Don't take this lightly, and if you don't unterstand what I've been saying here, read up on DNS in the meantime. You won't have much joy with your mail server without a reasonable grasp of reverse DNS, DNS caching, and MX records.

    Use the opportunity to set the TTL of the MX record(s) for your domain(s) to a few minutes perhaps. Once you have configured your mail system, you can then quickly change where other hosts will deliver their mail for your domain(s) and raise the TTLs again.

    Exim4

    Debian has come with the mail transfer agent (MTA; the mail server proper if you will) exim4 installed by default for a long, long time, and I've been using it on many boxes to feed the smart hosts for as long as I've been using Debian. So, I'll not migrate to something else just because my mail server will talk to the world now. Still, you'll have to dpkg-reconfigure exim4-config. Much of what's being asked by that is well explained in the help texts. Just a few hints:

    • “General type of mail configuration” would obviously be “internet site“.
    • Mail name ought to be <your domain>; if you have multiple domains, choose the one you'd like to see if someone mails without choosing any.
    • Keep the IP addresses to listen on empty – you want other hosts to deliver mail on port 25. Technically, it would be enough to listen only on the address your MX record points to, but that's a complication that's rarely useful.
    • Relaying mail for non-local domains is what you want if you want to be a smart host yourself. You'll pretty certainly want to keep this empty as it's easy to mess it up, and it's easy to configure authenticated SMTP even on clients (also see client connections on avoiding authenticated SMTP on top).
    • Exim also is a mail delivery agent (MDA), i.e., something that will put mail for domains it handles into people's mail boxes. I'll assume below that you select Maildir format in home directory as the delivery method. Maildir is so much cooler than the ancient mboxes, and whoever wants something else can still use .forward or procmail.
    • And do split your configuration into small files. Sure, you'll have to remember to run update-exim4.conf after your edits, but that litte effort will be totally worth it after your next dist-upgrade, when you won't have to merge the (large) exim4 config file manually and figure out what changes you did where.

    DNS Edits

    With this, you should be in business for receiving mail. Hence, make your MX record point to your new mail server. In an NSD zone file (and NSD is my choice for running my DNS server), this could look like:

    <your domain>.  IN MX 10 <your domain>.
    

    (as usual in such files: Don't forget the trailing dots).

    A couple of years ago, it was all the craze to play games with having multiple MX records to fend off spam. It's definitely not worth it any more.

    While I personally think SPF is a really bad idea, some spam filters will regard your mail more kindly if they find an SPF record. So, unless you have stronger reasons to not have one than just “SPF is a bad concept and breaks sane mailing list practices, .forward files and simple mail bouncing”, add a record like this:

    <your domain>.                3600    IN      TXT     "v=spf1" "+mx" "+a" "+ip4:127.0.0.1" "-all"
    

    – where you have to replace the 127.0.0.1 with your IP and perhaps add a similar ip6 clause. What this means: Mail coming from senders in <your domain> ought to originate at the IP(s) given, and when it comes from somewhere else it's fishy. Which is why this breaks good mailing list practices. But forunately most spam filters know that and don't interpret these SPF clauses to narrow-mindedly.

    SSL

    I'm not a huge fan of SSL as a base for cryptography – X.509 alone is scary and a poor defense against state actors –, but since it's 2021, having non-SSL services doesn't look good. Since it's important to look good so people accept your mail, add SSL to your exim installation.

    Unless you can get longer-living, generally-trusted SSL certificates from somewhere else, use letsencrypt certificates. Since (possibly among others) the folks from t-online.de want to see some declaration who is behind a mail server on such a web site, set up a web server for mail.<your-domain> and obtain letsencrypt SSL certificates for them in whatever way you do that.

    Then, in the post-update script of your letsencrypt updater, run something like:

    /bin/cp mail.crt mail.key /etc/exim4/ssl/
    /usr/sbin/service exim4 restart
    

    (which of course assumes that script runs as root or at least with sufficient privileges). /etc/exim4/ssl you'll have to create yourself, and to keep your key material at least a bit secret, do a:

    chown root:Debian-exim /etc/exim4/ssl
    chmod 750 /etc/exim4/ssl
    

    – that way, exim can read it even if it's already dropped its privileges, but ordinary users on your box cannot.

    Then tell exim about your keys. For that, use some file in /etc/exim4/conf.d/main; such files are the main way of configuring the exim4 package in non-trivial ways. I have 00_localmacros, which contains:

    MAIN_TLS_ENABLE = yes
    MAIN_TLS_CERTIFICATE = /etc/exim4/ssl/mail.crt
    MAIN_TLS_PRIVATEKEY = /etc/exim4/ssl/mail.key
    

    – that ought to work for you, too.

    Then, do the usual update-exim4.conf && service exim4 restart, and you should be able to speak SSL with your exim. The easiest way to test this is to install the swaks package (which will come in useful when you want to run authenticated SMTP or similar, too) and then run:

    swaks -a -tls -q HELO -s mail.<your domain> -au test -ap '<>'
    

    This will spit out the dialogue with your mail server and at some point say 220 TLS go ahead or so if things work, some more or less helpful error message if not.

    Aliases

    Exim comes with the most important aliases (e.g., postmaster) pre-configured in /etc/aliases. If you want to accept mail for people not in your /etc/passwd, add them there.

    The way this is set up, exim ignores domains; if you told exim to accept mails for domain1.de and domain2.fi, then mail to both user@domain1.de and user@domain2.fi will end up in /home/user/Maildir (or be rejected if user doesn't exist and there's no alias either). If you want to have domain-specific handling, add a file /etc/exim4/forwards that contains pairs like:

    drjekyll@example.org: mrhyde@otherexample.org
    

    The standard Debian configuration of Exim will not evaluate this file; to make it do that, drop a file wil something like:

    # Route using a global incoming -> outgoing alias file
    
    global_aliases:
      debug_print = "R: global_aliases for $local_part@$domain"
      driver = redirect
      domains = +local_domains
      allow_fail
      allow_defer
      data = ${lookup{$local_part@$domain}lsearch{/etc/exim4/forwards}}
    

    into (say) /etc/exim4/conf.d/router/450_other-aliases. After the usual update-exim4.conf, you should be good to go.

    Client Connections

    This setup only accepts mail for transport locally, and it will only deliver locally. That is: This isn't a smarthost setup yet.

    For delivery from remote systems, we're using ssh because pubkey auth is cool. This even works from an exim on the remote system …

  • OpenSSL: get_name: no start line?

    As part of my DIY mail server project, the other day I put a POP3 server on that box – solid-pop3d if you want to know –, and since that server doesn't have SSL built in, I configured stunnel to provide that, re-using a certificate I get for mail.tfiu.de's https server from letsencrypt. Trivial configuration:

    [spop3d]
    accept=995
    connect=110
    cert=/etc/stunnel/mail.pem
    

    And bang!, an error message from stunnel:

    [ ] Loading private key from file: /etc/stunnel/mail.pem
    [!] error queue: 140B0009: error:140B0009:SSL routines:SSL_CTX_use_PrivateKey_file:PEM lib
    [!] SSL_CTX_use_PrivateKey_file: 909006C: error:0909006C:PEM routines:get_name:no start line
    

    One of my least favourite pastimes is figuring out cryptic OpenSSL error messages, and so I immediately fed this to $SEARCH_ENGINE. The responses were, let's say, lacking rigour, and so I thought I might use this blog to give future message googlers an explanation of what the problem was in my case.

    What OpenSSL was saying here simply was: there's no private key in the PEM.

    Where would the fun be if OpenSSL had said that itself?

    In case this doesn't immediately tell you how to fix things: “PEM files” in today's computing [1] are typically bundles of a “key” (that's the pair of public and secret key in sensible language), a “certificate” (that's a signed public key in sensible language), and possibly intermediate certificates that user agents may need to figure out that the signature on the certificate is any good, based on what certificate authorities they trust.

    All these things almost always come in base64 encoded ASCII these days (that's the actual meaning of “PEM“), which is nice because you can create your “PEM file” with cat if you've got the other parts. For instance, in my dealings with letsencrypt, I'm creating the key using:

    openssl genrsa 4096 > $SERVERNAME.key
    

    Then I build a certificiate signing request in some way that's immaterial here, and finally call the great acme-tiny something like:

    acme-tiny --account-key ./account.key --csr ./"$SERVERNAME".csr \
            --acme-dir /var/www/acme-challenge\
             > ./"$SERVERNAME".crt
    

    Letsencrypt also hands out the the intermediate certificates at a well-known URI, so I pull that, too:

    curl https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem \
            > intermediate.pem
    

    With that, all I have to do to make the “PEM file” is:

    cat $SERVERNAME.crt intermediate.pem > $SERVERNAME.pem  # not
    

    That was basically what I had in my certificate updating script, and that is what caused the error in my case. Spot it? Right, I failed to cat the key file in. I should have written:

    cat $SERVERNAME.key $SERVERNAME.crt intermediate.pem > $SERVERNAME.pem
    

    So – if you're seeing this error message, while I can't say why your key pair is missing in the PEM, I'd strongly suspect it is. Diagnosis: look for

    -----BEGIN RSA PRIVATE KEY-----

    somewhere in the file (and make sure all the dashes are present if you see something that looks like that and you're still seeing the odd OpenSSL message).

    [1]

    I've had to look that up myself: PEM actually has nothing to do with all kinds of cryptographic material cat-ed together into one file. Rather, it stands for Privacy-Enhanced Mail, something the IETF tried to establish in the early 1990ies where today (regrettably) S/MIME sits and what we could all mercifully forget if people finally just adopted PGP already.

    RFC 1421 – where a good deal of PEM is defined – was published in 1993 and still talks about BITNET! Oh wow. While this sort of PEM is dead, it did pioneer the ASCII-armoring of X.509 material. Of course, ASCII-armoring as such had been around for many years at that time – let me just mention uuencode, the cornerstone of software distribution on Usenet –, and PGP had even used base64 for crypto stuff, but all these (sensibly) steered clear of X.509.

    And ASCII-armored X.509 is PEM's legacy, as acknowledged by RFC 7468 (published in 2015, more than 20 years after the original PEM). Of course, RFC 7468 doesn't mention the .pem extension, let alone anything about the practice of assembling multiple kinds of cryptographic material in files with that extension.

  • Perhaps I should be moving to gentoo

    I'm reading PDFs quite a bit, most of them in my beloved zathura. So, I was dismayed when today I paged through a book that showed in zathura as on the left side of this figure:

    Renderings of a PDF in poppler and mupdf.

    The metrics are off so badly that readability suffers.

    Rather than try to fix the PDF, I remembered I had for a long time wanted to look into using mupdf as a backend for zathura rather than its default poppler, if only because poppler used to have a shocking amount of rather serious bugs a couple of years ago (now that I think of it: It's been a while since I last heard of any – hm).

    Bringing up the PDF in mupdf looked a lot better (the right panel in the above figure). Which then led to a bout of yak shaving, because there is a plugin for zathura that promised to do what I wanted, zathura-pdf-mupdf, but of course nobody has bothered to package it up for Debian yet. So… let's try to build it.

    It's probably not a good sign that the package's README talks about make to build the thing, whereas the web page talks about a build system with commands meson and ninja (that, frankly, I had never heard about before, but at least it's in Debian). But, never mind, let's do meson build && cd build && ninjia (oh wow).

    Of course, building fails with something like:

    ../zathura-pdf-mupdf/index.c: In function ‘build_index’:
    ../zathura-pdf-mupdf/index.c:68:7: error: unknown type name ‘fz_location’; did you mean ‘fz_catch’?
           fz_location location = fz_resolve_link(ctx, document, outline->uri, &x, &y);
           ^~~~~~~~~~~
    

    A quick web search shows that this fz_location is part of the mupdf API and has indeed undergone an API change. So, I backported libmupdf from Debian testing (I'm on Debian stable almost always), and because that needs mujs, I backported that, too. Mujs sounds a lot like javascript in PDF, and that's where I first think gentoo: with its USE flags it would proabably make it easier to just keep javascript out of my PDF rendering engines altogether. Which is something I'd consider an excellent idea.

    Anyway, with a bit of hacking around – I don't have a libmupdf-third library that the meson build file mentions but perhaps doesn't need any more – I then got the plugin to build.

    Regrettably, zathura still would not use mupdf to render, saying:

    error: Could not load plugin '/usr/lib/i386-linux-gnu/zathura/libpdf-mupdf.so'
    (/usr/lib/i386-linux-gnu/zathura/libpdf-mupdf.so:
    undefined symbol: jpeg_resync_to_restart).
    

    Again asking a search engine about typical scenearios that would lead to this failure when loading a plugin, there's quite a bit of speculation, one of it being about using libjpeg-turbo instead of libjpeg. Which made me see what this plugin links again. Fasten your seat belts:

    $ ldd /usr/lib/i386-linux-gnu/zathura/libpdf-mupdf.so
            linux-gate.so.1 (0xf7fa7000)
            libgirara-gtk3.so.3 => /usr/lib/i386-linux-gnu/libgirara-gtk3.so.3 (0xf5d23000)
            libcairo.so.2 => /usr/lib/i386-linux-gnu/libcairo.so.2 (0xf5bd3000)
            libglib-2.0.so.0 => /usr/lib/i386-linux-gnu/libglib-2.0.so.0 (0xf5a9a000)
            libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xf58bc000)
            libgtk-3.so.0 => /usr/lib/i386-linux-gnu/libgtk-3.so.0 (0xf50bc000)
            libgdk-3.so.0 => /usr/lib/i386-linux-gnu/libgdk-3.so.0 (0xf4fae000)
            libpango-1.0.so.0 => /usr/lib/i386-linux-gnu/libpango-1.0.so.0 (0xf4f5f000)
            libgio-2.0.so.0 => /usr/lib/i386-linux-gnu/libgio-2.0.so.0 (0xf4d57000)
            libgobject-2.0.so.0 => /usr/lib/i386-linux-gnu/libgobject-2.0.so.0 (0xf4cf2000)
            libjson-c.so.3 => /usr/lib/i386-linux-gnu/libjson-c.so.3 (0xf4ce5000)
            libpthread.so.0 => /lib/i386-linux-gnu/libpthread.so.0 (0xf4cc2000)
            libpixman-1.so.0 => /usr/lib/i386-linux-gnu/libpixman-1.so.0 (0xf4c12000)
            libfontconfig.so.1 => /usr/lib/i386-linux-gnu/libfontconfig.so.1 (0xf4bc5000)
            libfreetype.so.6 => /usr/lib/i386-linux-gnu/libfreetype.so.6 (0xf4b02000)
            libpng16.so.16 => /usr/lib/i386-linux-gnu/libpng16.so.16 (0xf4ac3000)
            libxcb-shm.so.0 => /usr/lib/i386-linux-gnu/libxcb-shm.so.0 (0xf4abe000)
            libxcb.so.1 => /usr/lib/i386-linux-gnu/libxcb.so.1 (0xf4a90000)
            libxcb-render.so.0 => /usr/lib/i386-linux-gnu/libxcb-render.so.0 (0xf4a81000)
            libXrender.so.1 => /usr/lib/i386-linux-gnu/libXrender.so.1 (0xf4a75000)
            libX11.so.6 => /usr/lib/i386-linux-gnu/libX11.so.6 (0xf4926000)
            libXext.so.6 => /usr/lib/i386-linux-gnu/libXext.so.6 (0xf4911000)
            libz.so.1 => /lib/i386-linux-gnu/libz.so.1 (0xf48f0000)
            librt.so.1 => /lib/i386-linux-gnu/librt.so.1 (0xf48e5000)
            libm.so.6 => /lib/i386-linux-gnu/libm.so.6 (0xf47df000)
            libpcre.so.3 => /lib/i386-linux-gnu/libpcre.so.3 (0xf4766000)
            /lib/ld-linux.so.2 (0xf7fa9000)
            libgmodule-2.0.so.0 => /usr/lib/i386-linux-gnu/libgmodule-2.0.so.0 (0xf4760000)
            libpangocairo-1.0.so.0 => /usr/lib/i386-linux-gnu/libpangocairo-1.0.so.0 (0xf4750000)
            libXi.so.6 => /usr/lib/i386-linux-gnu/libXi.so.6 (0xf473d000)
            libXcomposite.so.1 => /usr/lib/i386-linux-gnu/libXcomposite.so.1 (0xf4739000)
            libXdamage.so.1 => /usr/lib/i386-linux-gnu/libXdamage.so.1 (0xf4734000)
            libXfixes.so.3 => /usr/lib/i386-linux-gnu/libXfixes.so.3 (0xf472d000)
            libcairo-gobject.so.2 => /usr/lib/i386-linux-gnu/libcairo-gobject.so.2 (0xf4721000)
            libgdk_pixbuf-2.0.so.0 => /usr/lib/i386-linux-gnu/libgdk_pixbuf-2.0.so.0 (0xf46f4000)
            libatk-1.0.so.0 => /usr/lib/i386-linux-gnu/libatk-1.0.so.0 (0xf46cb000)
            libatk-bridge-2.0.so.0 => /usr/lib/i386-linux-gnu/libatk-bridge-2.0.so.0 (0xf4693000)
            libxkbcommon.so.0 => /usr/lib/i386-linux-gnu/libxkbcommon.so.0 (0xf464d000)
            libwayland-cursor.so.0 => /usr/lib/i386-linux-gnu/libwayland-cursor.so.0 (0xf4644000)
            libwayland-egl.so.1 => /usr/lib/i386-linux-gnu/libwayland-egl.so.1 (0xf463f000)
            libwayland-client.so.0 => /usr/lib/i386-linux-gnu/libwayland-client.so.0 (0xf4630000)
            libepoxy.so.0 => /usr/lib/i386-linux-gnu/libepoxy.so.0 (0xf451e000)
            libharfbuzz.so.0 => /usr/lib/i386-linux-gnu/libharfbuzz.so.0 (0xf4407000)
            libpangoft2-1.0.so.0 => /usr/lib/i386-linux-gnu/libpangoft2-1.0.so.0 (0xf43ee000)
            libXinerama.so.1 => /usr/lib/i386-linux-gnu/libXinerama.so.1 (0xf43e7000)
            libXrandr.so.2 => /usr/lib/i386-linux-gnu/libXrandr.so.2 (0xf43da000)
            libXcursor.so.1 => /usr/lib/i386-linux-gnu/libXcursor.so.1 (0xf43cd000)
            libthai.so.0 => /usr/lib/i386-linux-gnu/libthai.so.0 (0xf43c1000)
            libfribidi.so.0 => /usr/lib/i386-linux-gnu/libfribidi.so.0 (0xf43a5000)
            libdl.so.2 => /lib/i386-linux-gnu/libdl.so.2 (0xf439f000)
            libmount.so.1 => /lib/i386-linux-gnu/libmount.so.1 (0xf4333000)
            libselinux.so.1 => /lib/i386-linux-gnu/libselinux.so.1 (0xf4306000)
            libresolv.so.2 => /lib/i386-linux-gnu/libresolv.so.2 (0xf42ec000)
            libffi.so.6 => /usr/lib/i386-linux-gnu/libffi.so.6 (0xf42e2000)
            libexpat.so.1 => /lib/i386-linux-gnu/libexpat.so.1 (0xf42a5000)
            libuuid.so.1 => /lib/i386-linux-gnu/libuuid.so.1 (0xf429b000)
            libXau.so.6 => /usr/lib/i386-linux-gnu/libXau.so.6 (0xf4296000)
            libXdmcp.so.6 => /usr/lib/i386-linux-gnu/libXdmcp.so.6 (0xf428f000)
            libdbus-1.so.3 => /lib/i386-linux-gnu/libdbus-1.so.3 (0xf4230000)
            libatspi.so.0 => /usr/lib/i386-linux-gnu/libatspi.so.0 (0xf41fb000)
            libgraphite2.so.3 => /usr/lib/i386-linux-gnu/libgraphite2.so.3 (0xf41cd000)
            libdatrie.so.1 => /usr/lib/i386-linux-gnu/libdatrie.so.1 (0xf41c3000)
            libblkid.so.1 => /lib/i386-linux-gnu/libblkid.so.1 (0xf4163000)
            libbsd.so.0 => /usr/lib/i386-linux-gnu/libbsd.so.0 (0xf4144000)
            libsystemd.so.0 => /lib/i386-linux-gnu/libsystemd.so.0 (0xf4099000)
            liblzma.so.5 => /lib/i386-linux-gnu/liblzma.so.5 (0xf406d000)
            liblz4.so.1 => /usr/lib/i386-linux-gnu/liblz4.so.1 (0xf404d000)
            libgcrypt.so.20 => /lib/i386-linux-gnu/libgcrypt.so.20 (0xf3f6a000)
            libgpg-error.so.0 => /lib/i386-linux-gnu/libgpg-error.so.0 (0xf3f45000)
    

    Now, I appreciate that glueing two pieces of relatively complex code together can be a bit involved, but: 69 libraries!? Among them Xrandr, Xinerama, wayland (which I don't use), systemd (even if I used it: what would that plugin have to do with it?), gpg-error, selinux, and then some things I've never heard about.

    I'm sorry, but this is wrong. Which makes me think hard if gentoo's USE flags might not really be the way to go in this day and age of exploding dependencies.

    Holy cow.

    In case you came here from a search engine that hit on one of the error messages: After writing this, I was hungry and let it sit. The one thing I can tell you is that the elusive jpeg_resync_to_restart is in libjpeg, indeed. What I don't know yet is why that library hasn't made it into the heap of libraries that the plugin links to.

    I'd suspect that zathura and mupdf are built against different libjegs – but then I'd have to explain how that would have happened. Hm.

    Nachtrag (2021-02-13)

    Well, I couldn't let it sit, so here's what I needed to do (and I suspect I'd have found it in one of the upstream branches):

    1. The libjpeg thing really is that the libjpeg library needs to be linked into the plugin in Debian; the details I can't quite work out, because I'd say the linker should be able to work that out, but clearly it's not, because the situation is actually being cared for in the plugin's meson file. However, you need to manually flip a switch: this would be a ./configure run in autoconf, but here, the command line is:

      meson setup --wipe -D link-external=true  build
      
    2. However, one link-time dependency is missing with the mupdf from Debian bullseye, and that's the spooky mujs. To fix this, patch the build file like so:

      diff --git a/meson.build b/meson.build
      index 23cdc6a..24929ca 100644
      --- a/meson.build
      +++ b/meson.build
      @@ -19,8 +19,8 @@ zathura = dependency('zathura', version: '>=0.3.9')
       girara = dependency('girara-gtk3')
       glib = dependency('glib-2.0')
       cairo = dependency('cairo' …
  • Fürchten lernen 2: Ihre Reputation ist nicht verfügbar

    In Teil 2 der Saga von einem, der Mails verschicken will geht es um mailbox.org – an sich ja einer eher sympatische Firma, gerade im Vergleich zu Telekom und Vodafone. Aber, beim Einliefern:

    2021-02-08 05:52:09 1l8yWu-0006Ry-EM ** (Adresse tut nichts zur Sache)
    R=dnslookup T=remote_smtp H=mxext2.mailbox.org [80.241.60.215]
    X=TLS1.3:ECDHE_RSA_AES_256_GCM_SHA384:256 CV=yes DN="C=DE,ST=Berlin,
    L=Berlin,O=Heinlein Support GmbH,OU=MBO,CN=*.mailbox.org": SMTP error
    from remote mail server after RCPT TO:<Adresse tut nichts zur Sache>:
    554 5.7.1 Service unavailable; Client host [116.203.206.117] blocked
    by RBL; http://www.barracudanetworks.com/reputation/?pr=1&ip=116.203.206.117
    

    Mit anderen Worten: Eine „Real Time Black List“ RBL fand, dass die IP nach Spam riecht, und drum darf ich nicht einliefern.

    Hu? Von dieser IP kam seit mindestens einem Jahr (also seit ich sie habe) nicht eine Mail an öffentliche SMTP-Server (von meinem Smarthost abgesehen), und schon gar nichts, was auch nur entfernt nach Spam aussah. Wie kommt die Kiste dann auf diese Blacklist? Schaun wir mal und dereferenzieren die URL. Resultat: Weiße Seite. Ach nee, soll ich denen Javascript erlauben? Gibt immer noch eine weiße Seite. Local Storage? Weiße Seite. Curl anwerfen. Nach kurzer Wartezeit kommen genau null Bytes zurück.

    Echte Profis, die barracudas. Vertrauenswürdige Treuhänder unserer Mail-Infrastruktur.

    Also gut, tippe ich halt mal barracudanetworks.com in den Browser ein (na gut, in Wirklichkeit habe ich in luakit 2gu gesagt; so viel nerd cred muss an der Stelle sein). Kommt ein animierter Security-Salespitch (yikes! Habe ich denen gerade Javascript erlaubt?), den sich niemand antun müssen sollte – „It's time to protect your business“ my ass. Und klar, google analytics ist auch gleich mit drauf.

    Wie finde ich denn jetzt raus, warum der Laden mich für einen Spammer hält? Auch mit etwas Rumklicken: nichts außer dämlichen Testimonials („The intuitive interface makes deployment simple and the bundled features combine to create a comprehensive email security solution.“).

    Und das alles nur, damit ich Mail einliefern darf?

    Tatsächlich kann mensch sich irgendwie zu barracudacentral.org durchklicken. Und da gibts dann einen Removal Request, der eigentlich aussieht wie ein Phishing-Versuch:

    Screenshot Formular

    Wozu wollen die jetzt eine Telefonnummer haben? Wozu eigentlich eine Mailadresse? Wie kann das bei der Prüfung eine Rolle spielen? Rufen die wirklich zurück? Und wenn ja: wärs nicht freundlich, wenigstens nach der Zeitzone zu fragen?

    Wenn ich den mailbox.org-Leuten nicht ziemlich arg trauen würde, wäre ich jetzt weg. Aber ich probiers mal. „Removal requests are typically investigated and processed within 12 hours of submission“ sagen die. Ich bin ja mal neugierig.

  • Das Fürchten lernen

    Nachdem ich in einer Zeit, als es üblich war, einfach von überall her Mails anzunehmen, tatsächlich mal ein ganz klassisches Sendmail betrieben habe, hatte ich seit den späten 90er Jahren nur noch Mailserver, die bei Smart Hosts eingeliefert haben, also kurzerhand alle ausgehende Mail an den gleichen anderen Server weitergereicht haben. – und umgekehrt auch alle Mails von dort bezogen.

    Im letzten Jahr allerdings hat das immer weniger funktioniert. Irgendwie scheint es, als hätten mit Corona die diversen Mail-Betreiber immer gewagtere Politiken ausgerollt, die vielleicht ein wenig Spam, vor allem aber die Zuverlässigkeit von Mail bekämpften; gleichzeitig nimmt die Bereitschaft erkennbar ab, die Weisheit dieser Politiken nochmal zu überdenken und im Fall von Fehlfunktionen zu klären, was eigentlich kaputt ist.

    Gegen die Politiken kann ich nichts tun, gegen das stille Verschlucken von Mails gibts eine Lösung: Ich muss wieder selbst einen richtigen, direkt ausliefernden Mailserver betreiben. Denn das ist ja eigentlich das Schöne an offenen Standards: JedeR kanns selbst machen.

    Allerdings: einfach einen Server aufsetzen und los gehts, das ist im kommerzialisierten Internet nur noch selten möglich (Mumble sei hier mal als löbliche Ausnahme erwähnt). Im Web zum Beispiel gehts ohne https und die damit zusammenhängenden Verrenkungen kaum noch, von den mittlerweile üblichen Labyrinthen aus Reverse Proxies und Containerfarmen ganz zu schweigen.

    Bei Mail, so scheint es, ist es noch viel schlimmer; ich hatte ja schon damit gerechnet, dass es da und dort etwas fummelig würde in Zeiten von SPF, DKIM, DANE und so fort.

    Dass es aber so schlimm ist, hätte ich nicht gedacht. Und so glaube ich, dass dieses Blog mutieren wird zu einer Geschichte von einem, der auszog, das Fürchten zu lernen – und insbesondere, warum immer weniger Leute eigene Infrastruktur betreiben und sich das Internet immer weiter zentralisiert.

    Kapitel 1: t-online.de

    Einliefern bei t-online.de:

    2021-02-07 11:27:26 1l8hHU-00087u-Mk H=mx00.t-online.de [194.25.134.8]:
    SMTP error from remote mail server after initial connection:
    554 IP=116.203.206.117 - A problem occurred. (Ask your postmaster
    for help or to contact tosa@rx.t-online.de to clarify.)
    

    Na klasse. „A problem occurred“. Auch mit richtig viel Mühe kann ich mir keine weniger Informative Fehlermeldung vorstellen.

    Duckduckgo berichtet, dass das halt einfach so ein whitelisting der Telekom ist und dass mensch den Laden freundlich bitten muss. Wie bitte? Was machen die eigentlich, wenn irgendwer aus Singapur einliefert? Erwarten die ernsthaft, dass auch so ein Laden bei ihnen anklopft?

    So kann mensch Standards natürlich auch aushöhlen.

    Aber immerhin: selbst am Samstag abend um acht antwortet jemand auf Kontaktmails – es sieht ganz so aus, als hätte die Telekom das an irgendwelche Kontraktoren ausgelagert. Und die wollen, dass an dem Mailserver ein Webserver hängt, auf dem, so sieht es aus, ein Impressum nach Medienstaatsvertrag liegen soll. Wozu? Keine Ahnung. Ich habe gefragt und keine sinnvolle Antwort bekommen. Und mit welchem Recht ein doch recht großer Laden einfach anderen Leuten Vorschriften machen will, welche Daten sie zu publizieren haben, bleibt natürlich offen.

    Tatsächlich bin ich in dem Punkt auch etwas empfindlich, denn die Hartleibigkeit, mit der die Exekutive im Medienstaatsvertrag den erklärten Willen des Bundestags aus dem Telemediengesetz auszuhebeln versucht... nun, das ist Thema für einen anderen Post.

    Na ja, zumindest für t-online.de sollte es mein alter Smarthost noch eine Weile tun; kriege ich halt weiter nicht mit, wenn da was kaputt ist.

    Mein MTA, exim4, hat auf Debian-Systemen die praktische Datei /etc/exim4/hubbed.hosts. Da steht t-online.de jetzt erstmal drin. Ich werde ein andermal über unnötige Komplexität jammern.

    Kapitel 2: arcor.de

    Einliefern bei arcor.de:

    2021-02-07 17:41:20 1l8n7d-0001gH-Qj ** <elided>@arcor.de R=dnslookup
    T=remote_smtp H=mx2.vodafonemail.de [2.207.150.241]: SMTP error
    from remote mail server after initial connection:
    554 fra1frontrelay13.vodafonemail.de ESMTP not accepting messages
    

    Was ist das jetzt schon wieder für eine Teufelei? Duckduckgo führt auf eine Forendiskussion die ein Vodafon-Mitarbeiter wie folgt beendet:

    wir befinden uns mitten in dem Umzug zu einem anderen Mailbetreiber. Die Arbeiten sind ca bis Ende Janaur [aus dem Kontext ist 2021 abzuleiten – A.] abgeschlossen.

    Ich geh davon aus, dass die Emailadresse Deiner Homepage auf einer Blacklist steht und daher von unserem Spamfilter aussortiert wird.

    Dafür kann ich zum jetzigen Stand kein technisches Ticket aufnehmen.

    Wie bitte? „Kein technisches Ticket“? Für eine doch recht drastische Fehlfunktion eines wirklich fundamentalen Internetdiensts, nämlich E-Mail?

    Au weia. Nun, kommt arcor.de halt auch erstmal in die Hubbed Hosts. Vielleicht probiere ich es Anfang März nochmal, kann ja sein, dass dann die Zeit ist für ein „technisches Ticket aufnehmen“.

    Es sieht nicht gut aus für offene Standards.

  • An Xlib-based Screen Ruler in Python

    In my bad prediction (in both senses of the word) on what we'll see in the intensive care stations I've mentioned a screen ruler I've written.

    Well, since I've still not properly published it and I doubt I'll ever do that without further encouragement, let me at least mention it here. It's pyscreenruler.

    Nachtrag (2024-02-24)

    Actually, I have put it on codeberg now: https://codeberg.org/AnselmF/pyscreenruler.

    I've written it because all the other screen rulers for X11 I found in Debian (and a bit beyond) would only let you measure either horizontal or vertical lines. I, however, needed to measure slopes, as in this part of the curve for the SARS-2 patient count in German critical care stations:

    Plot: curve with a ruler

    – which, by the way, shows that, surprisingly to me, our patient numbers still go down exponentially (if slowly, with a halving time of almost two months). Hm. But today's point is pyscreenruler, which is what has produced the ruler in that image.

    When I wrote this last November, I quickly found Tkinter (which is what I still usually use to write quick graphical hacks) doesn't really support shaped windows – and anything but shaped windows would, I figured, be extremely painful here. Plain Xlib would make the shaping part relatively straighforward. So, I based the thing directly on the Xlib even for the remaining code.

    That brought forth fond memories of programs I wrote on Atari ST's GEM in the late 1980ies. For instance, with the explicit event loop; on the left code from November 2021, on the right code I touched last in March 1993 (according to the time stamp):

    def loop(self):                     void event_loop(watch *clstr)
       drag_start = False               { [...]
       while 1:
           e = self.d.next_event()        do
                                           { event =  evnt_multi(
           if e.type==X.DestroyNotify:        MU_MESAG|(clstr->on?MU_TIMER:0),
               sys.exit(0)                      2,0x0,1, [...]
                                              if (event&MU_MESAG)
           elif e.type==X.Expose:               handle_message(pipe,clstr);
               [...]                          if (event&MU_TIMER)
           elif e.type==X.KeyPress:             updclck(clstr);
               [...]                       }
           elif e.type==X.ButtonPress:     while(1);
               [...]                    }
    

    I'd not indent C like on the right any more.

    For the record, Xlib's next_event call looks simpler that GEM's evnt_multi because you configure all the various parameters – what events to listen for, timeouts, and so forth – in other calls.

    Oh, the contortions one has to go through to have updates when a part of the window is exposed (I'm playing it simple and just use offscreen pixmaps)! Or the fact that the whole thing will (I think) not run on displays with 8 and 16 bits of colour. Or the (seemingly) odd behaviour of programs if you do not at least implicitly wait for the window to be mapped (in actual pyscreenrules, I sidestep the problem by deferring all painting to the screen to the expose handler). And X won't tell you about mapping unless you ask for it. And so on. Note to self: Next time you write an Xlib program, let Christophe Tronche remind you of the various little quirks before hacking off.

    Anyway: If you need a rotatable ruler, install the python3-xlib package (that's really all it needs), download pyscreenruler, make it executable and start it. If you click and drag in the inner half of the ruler, you can move it around, if you click and drag on the outer parts, you can rotate the thing (which you can also do by hitting + and -).

    What's missing: Well, I could imagine letting people change the length of the thing (shift-drag on the edges, perhaps?), then giving length and angle in text somewhere. And it's thoroughly pixel-based right now, which it shouldn't be when there's displays with 200 dpi and more out there.

    Send mails and/or patches to make me properly fix it up. Since I've really missed it, I could even imagine packing it up for Debian...

« Seite 4 / 4

Letzte Ergänzungen