Category Archives: Linux

Atomically switch a directory tree served by nginx

This post briefly demonstrates how to atomically switch a directory tree of static files served by nginx.

Consider the following minimal nginx config file:

$ cat conf/nginx.conf 
events {
    use epoll;
}
 
http {
    server {
        listen 0.0.0.0:80;
        location / {
            root /static/current ;
        }
    }
}

The goal is to replace the directory /static/current atomically while nginx is running.

This snippet shows the directory layout that I started out with:

$ tree
.
├── conf
│   └── nginx.conf
└── static
    ├── version-1
    │   └── hello.html
    └── version-2
        └── hello.html

conf/nginx.conf is shown above. The static directory contains two sub trees, and the goal is to switch from version-1 to version-2.

For this demonstration I have started a containerized nginx from its official Docker image:

$ docker run -v $(realpath static):/static:ro -v $(realpath conf):/etc/nginx:ro -p 127.0.0.1:8088:80 nginx nginx -g 'daemon off;'

This mounts the ./static directory as well as the nginx configuration file into the container, and exposes nginx listening on port 8088 on the local network interface of the host machine.

Then, in the ./static directory one can choose the directory tree served by nginx by setting a symbolic link, and one can subsequently switch the directory tree atomically, as follows:

1) No symbolic link is set yet — leading to a 404 HTTP response (the path /static/current does not exist in the container from nginx’ point of view):

$ curl http://localhost:8088/hello.html
<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>nginx/1.15.6</center>
</body>
</html>

2) Set the current symlink to serve version-1:

$ cd static
$ ln -s version-1 current && curl http://localhost:8088/hello.html
hello 1

3) Prepare a new symlink for version-2 (but don’t switch yet):

$ ln -s version-2 newcurrent

4) Atomically switch to serving version-2:

$ mv -fT newcurrent current && curl http://localhost:8088/hello.html
hello 2

In step (4) It is essential to use mv -fT which changes the symlink with a rename() system call. ln -sfn would also appear to work, but it uses two system calls under the hood and therefore leaves a brief time window during which opening files can fail because the path is invalid.

Final directory layout including the symlink current (currently pointing to version-2):

$ tree
.
├── conf
│   └── nginx.conf
└── static
    ├── current -> version-2
    ├── version-1
    │   └── hello.html
    └── version-2
        └── hello.html

Kudos to https://rcrowley.org/2010/01/06/things-unix-can-do-atomically.html for being a great reference.

Download article as PDF file from Elsevier’s ScienceDirect via command line (curl)

When not in the office, we often times cannot directly access scientific literature, because access control is usually based on IP addresses. However, we usually have SSH access to the university network. Being logged in to a machine in the university network we should — in theory — be able to access a certain article. Most of the times it is the PDF file that we are interested in and not the “web page” corresponding to an article. So, can’t we just $ curl http://whatever.com/article.pdf to get that file? Most of the times, this does not work, because access to journal articles usually happens through rather complex web sites, such as Elsevier’s ScienceDirect:

ScienceDirect is a leading full-text scientific database offering journal articles and book chapters from nearly 2,500 journals and 26,000 books.

Such web sites add a considerable amount of complexity to the technical task of downloading a file. The problem usually starts with obtaining the direct URL to the PDF file. Also, HTTP redirection and cookies are usually involved. Often times, the only solution people see to solve these issues is to set up a VPN and then to use a fully fledged browser through that VPN, and let the browser deal with the complexity.

However, I prefer to get back to the basics and always strive to somehow find a direct URL to the PDF file to then download it via curl or wget.

This is my solution for Elsevier’s ScienceDirect:

Say, for instance, you wish to download the PDF version of this article: http://www.sciencedirect.com/science/article/pii/S0169433215012131

Then all you need is that URL and the following commands executed on a common Linux system:

export SDURL="http://www.sciencedirect.com/science/article/pii/S0169433215012131"
curl -Lc cookiejar "${SDURL}" | grep pdfurl | perl -pe 's|.* pdfurl=\"(.*?)\".*|\1|' > pdfurl
curl -Lc cookiejar "$(cat pdfurl)" > article.pdf

The method first parses the HTML source code of the main page corresponding to the article and extracts a URL to the PDF file. At the same time, it also stores the HTTP cookie(s) set by the web server when accessing named web page. These cookies are then re-used when accessing the PDF file directly. This has reproducibly worked for me.

If it does not work for you, I recommend having a look into the file pdfurl and see if that part of the process has lead to a meaningful result or not. Obviously, the second step can only succeed aver having obtained a proper URL to the PDF file.

This snippet should not be treated as a black box. Please execute it in an empty directory. Also note that this snippet only works subject to the condition that ScienceDirect keeps functioning the way it does right now (which most likely is the case for the next couple of months or years).

Don’t hesitate to get back to me if you have any questions!

Uploaded.to download with wget

Downloading from uploaded.to/.net with premium credentials through the command line is possible using standard tools such as wget or curl. However, there is no official API and the exact method required depends on the mechanism implemented by the uploaded.to/.net website. Finding these implementation details requires a little amount reverse engineering.

Here I share a small shell script that should work on all POSIX-compliant platforms (e.g. Mac or Linux). The method is based on current behavior of the uploaded.to website. There are no special tools involved, just wget, grep, sed, mktemp.

(The solutions I found on the web did not work (anymore) and/or were suspiciously wrong.)

Usage

Copy the script content below, define username and password, and save the script as, for instance, download.sh. Then, invoke the script like so:

$ /bin/sh download.sh urls.txt

The file urls.txt should contain one uploaded.to/.net URL per line, such as in this example:

http://uploaded.net/file/98389123/foo.rar
http://uploaded.net/file/bxmsdkfm/bar.rar
http://uploaded.net/file/72asjh98/not.zip

Method

This paragraph is just for the curious ones. The script first POSTs your credentials to http://uploaded.net/io/login and stores the resulting authentication cookie in a file. This authentication cookie is then used for retrieving the website corresponding to an uploaded.to file. That website contains a temporarily valid download URL corresponding to the file. Using grep and sed, the HTML code is filtered for this URL. The payload data transfer is triggered by firing a POST request with empty body against this URL (cookie not needed). Files are downloaded to the current working directory. All intermediate data is stored in a temporary directory. That directory is automatically deleted upon script exit (no data is leaked, unless the script is terminated with SIGKILL).

The script

#!/bin/sh
# Copyright 2015 Jan-Philip Gehrcke, http://gehrcke.de
# See http://gehrcke.de/2015/03/uploaded-to-download-with-wget/
 
 
USERNAME="user"
PASSWORD="password"
 
 
if [ "$#" -ne 1 ]; then
    echo "Missing argument: URLs file (containing one URL per line)." >&2
    exit 1
fi
 
 
URLSFILE="${1}"
if [ ! -r "${URLSFILE}" ]; then
    echo "Cannot read URLs file ${URLSFILE}. Exit." >&2
    exit 1
fi
if [ ! -s "${URLSFILE}" ]; then
    echo "URLs file is empty. Exit." >&2
    exit 1
fi
 
 
TMPDIR="$(mktemp -d)"
# Install trap that removes the temporary directory recursively
# upon exit (except for when this program retrieves SIGKILL).
trap 'rm -rf "$TMPDIR"' EXIT
 
 
LOGINRESPFILE="${TMPDIR}/login.response"
LOGINOUTPUTFILE="${TMPDIR}/login.outerr"
COOKIESFILE="${TMPDIR}/login.cookies"
LOGINURL="http://uploaded.net/io/login"
 
 
echo "Temporary directory: ${TMPDIR}"
echo "Log in via POST request to ${LOGINURL}, save cookies."
wget --save-cookies=${COOKIESFILE} --server-response \
    --output-document ${LOGINRESPFILE} \
    --post-data="id=${USERNAME}&pw=${PASSWORD}" \
    ${LOGINURL} > ${LOGINOUTPUTFILE} 2>&1
 
# Status code is 200 even if login failed.
# Uploaded sends a '{"err":"User and password do not match!"}'-like response
# body in case of error.
 
echo "Verify that login response is empty."
# Response is more than 0 bytes in case of login error.
if [ -s "${LOGINRESPFILE}" ]; then
    echo "Login response larger than 0 bytes. Print response and exit." >&2
    cat "${LOGINRESPFILE}"
    exit 1
fi
 
# Zero response size does not necessarily imply successful login.
# Wget adds three commented lines to the cookies file by default, so
# set cookies should result in more than three lines in this file.
COOKIESFILELINES="$(cat ${COOKIESFILE} | wc -l)"
echo "${COOKIESFILELINES} lines in cookies file found."
if [ "${COOKIESFILELINES}" -lt "4" ]; then
    echo "Expected >3 lines in cookies file. Exit.". >&2
    exit 1
fi
 
echo "Process URLs."
# Assume that login worked. Iterate through URLs.
while read CURRENTURL; do
    if [ "x$CURRENTURL" = "x" ]; then
        # Skip empty lines.
        continue
    fi
    echo -e "\n\n"
    TMPFILE="$(mktemp --tmpdir=${TMPDIR} response.html.XXXX)"
    echo "GET ${CURRENTURL} (use auth cookie), store response."
    wget --no-verbose --load-cookies=${COOKIESFILE} \
        --output-document ${TMPFILE} ${CURRENTURL}
 
    if [ ! -s "${TMPFILE}" ]; then
        echo "No HTML response: ${TMPFILE} is zero size. Skip processing."
        continue
    fi
 
    # Extract (temporarily valid) download URL from HTML.
    LINEOFINTEREST="$(grep post ${TMPFILE} | grep action | grep uploaded)"
    # Match entire line, include space after action="bla" , replace
    # entire line with first group, which is bla.
    DLURL=$(echo $LINEOFINTEREST | sed 's/.*action="\(.\+\)" .*/\1/')
    echo "Extracted download URL: ${DLURL}"
    # This file contains account details, so delete as soon as not required
    # anymore.
    rm -f "${TMPFILE}"
    echo "POST to URL w/o data. Response is file. Get filename from header."
    # --content-disposition should extract the proper filename.
    wget --content-disposition --post-data='' "${DLURL}"
done < "${URLSFILE}"

Systemdatenträger wechseln ohne “Augen zu und durch”-Mentalität

Einleitung

In diesem Artikel beschreibe ich wie man Windows in wenigen Schritten von einem Speichermedium zum Anderen überträgt — mit professionellen Werkzeugen. Das ist ein kritischer Prozess. Aber ein simpler Prozess, den man verstehen kann und sollte, bevor man ihn einleitet. Wie immer gibt es zahlreiche schändliche Anleitungen da draußen, bei denen dieser Prozess nicht hinreichend erklärt und irgendwelchen bunten Klick-Tools überlassen wird. Das Problem dabei: der Vorgang wird dem Nutzer übersimplifiziert präsentiert. Wichtige Entscheidungen trifft die angepriesene Software selbst, essentielle Details werden vom Nutzer ferngehalten. Aber im Bereich von Datenträgerstrukturen gibt es zu viele potentielle Komplikationen, deren Behandlung man keiner Pseudointelligenz oder auch der “Holzhammermethode” überlassen darf:

Holzhammermethode, zweckentfremdet von http://www.datamartist.com.

Holzhammermethode, zweckentfremdet von datamartist.com.

In professionellen Umgebungen wird ein solcher Migrationsprozess normalerweise akribisch geplant und jeder Schritt mit dem einen dafür etablierten Werkzeug ausgeführt. Ergebnis: die Migration gelingt schnell und zuverlässig.

In privaten Umgebungen herrscht Unsicherheit, man kennt die technischen Details nicht und folgt gerne einer simplen Abstraktion des Problems. Es herrscht Augen zu und durch-Mentalität. Grafisch hübsch aufgemachte Software-Assistenten suggerieren schließlich eine gewisse Sicherheit. Doch Hoffnung ist keine Strategie. Das etwas andere Ergebnis: mit einiger Wahrscheinlichkeit geht etwas schief und man verbringt Stunden oder Tage mit einer chaotischen Phase der Problemlösung, bei der vielleicht Dritte mit einbezogen werden müssen.

Eine grundsätzliche Ursache dieses Gefälles zwischen professioneller und privater Umgebung ist sicherlich der Mangel an Detailwissen in Letzterer. Damit einher geht aber — und das ist das eigentliche Übel — fehlende Anerkennung, dass ein solcher Prozess akribisch geplant werden muss. Ich schreibe das hier auf Deutsch und mit dieser kleinen Einleitung, um in meinem direkten Umfeld ein besseres Bewusstsein für diese Thematik und Problematik zu schaffen.

Es wird unten stehend demonstriert, wie ein solcher Prozess zuverlässig umgesetzt werden kann. Als Beispiel dient ein Spezialfall, den die meisten Windows-Nutzer zu Hause haben. Die Grundidee — Detailplanung und die verwendeten Ansätze und Werkzeuge — sind jedoch auch auf andere Situationen und Betriebssysteme anwendbar. Ihr werdet merken, wenn Eure Situation von der hier beschriebenen abweicht. Dann könnt Ihr mich gerne fragen. Dementsprechend erhebt die folgende Anleitung keinen Anspruch auf Vollständigkeit.

Migrationsprozess

1) Live-Linux präparieren (5 Minuten)

Ihr braucht einen USB-Stick mit einem modernen Live-Linux-System, welches die notwendigen Werkzeuge bereithält, um zuverlässig Daten auf Datenträgern zu analysieren und zu manipulieren. Ich empfehle die Linux-Distribution SystemRescueCD. Gibt es hier zum Herunterladen. Wie bekommt Ihr das auf einen USB-Stick, von dem der Rechner starten kann? Mit dem quelloffenen Rufus. Dort wählt Ihr “create a bootable disk” und bei “select ISO file” wird das SystemRescueCD-Abbild ausgewählt, was gerade heruntergeladen wurde.

2) Transfersystem vorbereiten (5 Minuten)

Der Quell-Datenträger (mit der bisherigen Windows-Installation) wird aus dem Produktivsystem herausgenommen. Der Ziel-Datenträger wird bereitgelegt.

Es wird ein Rechner gebraucht, in dem diese beiden Datenträger gleichzeitig angeschlossen werden. Dieser Rechner wird vom zuvor präparierten USB-Stick gestartet. Beim Start von SystemRescueCD wird der default Start-Modus gewählt, das Tastaturlayout (z. B. DE) definiert und die Desktop-Umgebung mittels startx gestartet.

In diesem Zustand ist der Zugriff auf alle Datenträger möglich. SystemRescueCD wagt jedoch keine autonomen Zugriffe. Ohne Anweisung wird kein einziges Dateisystem eingehängt. Insbesondere wird ohne konkreten Befehl auf keinen Datenträger schreibend zugegriffen. Das ist ein konzeptioneller Schutz vor ungeplanten Ereignissen.

3) Quelle und Ziel eindeutig identifizieren (2 Minuten)

Jedes Linux-Betriebssystem nummeriert die Datenträger intern durch. Jeder Datenträger und jede Partition ist per /dev/sd* lesend und schreibend erreichbar. Jetzt muss die Bezeichnung des bisherigen Windows-Datenträgers ausfindig gemacht werden. Es hilft eine Auflistung der verfügbaren Datenträger samt Partitionen. Ein praktischer Befehl ist fsarchiver probe simple, welcher in ein Terminal eingegeben wird. Das ist die Ausgabe in meinem Fall:

[======DISK======] [=============NAME==============] [====SIZE====] [MAJ] [MIN]
[sda             ] [KINGSTON SVP200S               ] [   111.79 GB] [  8] [  0]
[sdb             ] [WDC WD10EAVS-00D               ] [   931.51 GB] [  8] [ 16]
[sdc             ] [Samsung SSD 850                ] [   476.94 GB] [  8] [ 32]
[sdd             ] [RALLY2                         ] [     3.74 GB] [  8] [ 48]
[sde             ] [USB DISK                       ] [     7.55 GB] [  8] [ 64]
 
[=====DEVICE=====] [==FILESYS==] [======LABEL======] [====SIZE====] [MAJ] [MIN] 
[loop0           ] [squashfs   ] [<unknown>        ] [   278.66 MB] [  7] [  0] 
[sda1            ] [ntfs       ] [System Reserved  ] [   100.00 MB] [  8] [  1] 
[sda2            ] [ntfs       ] [<unknown>        ] [   111.69 GB] [  8] [  2] 
[sdb1            ] [ntfs       ] [Software         ] [   196.16 GB] [  8] [ 17] 
[sdb2            ] [ntfs       ] [Daten            ] [   735.35 GB] [  8] [ 18] 
[sdd1            ] [vfat       ] [SYSRCD-4_4_      ] [     3.74 GB] [  8] [ 49] 
[sde1            ] [vfat       ] [<unknown>        ] [     7.55 GB] [  8] [ 65]

Beim Lesen der Ausgabe braucht man ein bisschen Kenntnis über die eigene Hardware: in der oberen Liste sollten Gerätebezeichnung und Datenträgerkapazität genügend Aufschluss geben. Der bisherige Windows-Datenträger von Kingston heißt in diesem Fall sda. Er ist per Dateisystempfad /dev/sda erreichbar.

In der unteren Liste sind die automatisch erkannten Partitionen aufgeführt. Die Partition sda1 ist eine 100 MB große Partition mit dem Namen “System Reserved …”. Diese wird für gewöhnlich von Windows 7 & 8 bei der Installation angelegt und übernimmt essentielle Aufgaben (sie wird im Betrieb von Windows nicht als normaler Datenträger angezeigt aber muss bei der Migration mitgenommen werden). Die Partition sda2 ist das, was man aus Windows-Sicht als Systemlaufwerk bezeichnen würde (meist Laufwerk “C:”). Bei beiden Partitionen hat fsarchiver wie erwartet erkannt, dass sie ein NTFS-Dateisystem enthalten. Es ist keine weitere Partition auf /dev/sda vorhanden. Die Bestandsaufnahme der Quelle ist somit abgeschlossen.

/dev/sdc ist der Datenträger, der zukünftig Windows tragen soll. Bisher ist keine Partition auf diesem Datenträger eingerichtet.

4) Die Transferstrategie festlegen

Die Strategie im Groben: Die Inhalte beider Quell-Partitionen (/dev/sda1 und /dev/sda2) sollen auf Block-Ebene auf den neuen Datenträger kopiert werden. Vorher bekommt der neue Datenträger eine frische Partitionstabelle. Der Bootloader wird vom alten Datenträger auf den Neuen kopiert.

Die Strategie im Detail:

  • Per GNU fdisk wird eine neue Partitionstabelle vom Typ msdos auf /dev/sdc erstellt, sodass die Situation vom alten Datenträger in den wesentlichen Punkten repliziert wird:
    • /dev/sdc1 erhält die selbe Startposition und Größe wie /dev/sda1.
    • In diesem Fall ist der neue Datenträger größer als der alte: /dev/sdc2 soll den Rest des Datenträgers füllen.
    • Beide Partitionen erhalten eine Markierung vom Typ 7 (HPFS/NTFS/exFAT).
    • /dev/sdc1 wird als aktiv markiert.
  • Der Master Boot Record wird per dd nur teilweise vom alten Datenträger auf den neuen kopiert (nur der Bootloader und die Datenträgersignatur, nicht jedoch die Partitionstabelle).
  • /dev/sda1 wird blockweise per dd nach /dev/sdc1 kopiert.
  • /dev/sda2 wird blockweise per dd nach /dev/sdc2 kopiert.
  • Das NTFS-Dateisystem welches auf /dev/sda2 befindet hat nun mehr Platz auf /dev/sdc2. Es wird per ntfsresize entsprechend darauf aufmerksam gemacht, sodass es den verbleibenden Platz innerhalb der Partition nutzen kann. Zur Vereinfachung dieses Schritts wird als Wrapper GParted benutzt. Beide Tools sind quelloffen und Industriestandard.

Alle hier genannten Werkzeuge sind Teil der SystemRescueCD-Distribution.

5) Realisierung (5 Minuten Arbeit, X Minuten Warten)

  • Details der Partitionstabelle vom bisherigen Windows-Datenträger per fdisk -l auslesen:
    Device    Boot     Start       End    Blocks  Id System
    /dev/sda1 *         2048    206847    102400   7 HPFS/NTFS/exFAT
    /dev/sda2         206848 234438655 117115904   7 HPFS/NTFS/exFAT

    Beide Partitionen sind von Typ 7. /dev/sda1 ist Boot-markiert (“active”). Start und Ende sind präzise angegeben. Diese Parameter wollen wir exakt replizieren. Die einzige Änderung der neuen Tabelle gegenüber der Alten wird die Endmarkierung der Partition /dev/sda2 sein, da der neue Datenträger größer ist als der Alte.

  • Neue Partitionstabelle auf neuem Datenträger per fdisk /dev/sdc erstellen. fdisk wird schrittweise per Tastatur bedient:
    • o create a new empty DOS partition table
    • n add a new partition
    • p primary (0 primary, 0 extended, 4 free)
    • Partition number 1, first sector: 2048 (default), last sector: 206847
    • n add a new partition
    • p primary (0 primary, 0 extended, 4 free)
    • Partition number 2, first sector: default, last sector: default (bis zum Ende des Datenträgers)
    • mit Befehl t change a partition type wird Typ 7 für beide Partitionen gewählt.
    • mit Befehl a toggle a bootable flag wird Partition 1 aktiv markiert.
    • mit Befehl w write table to disk and exit werden die Änderungen geschrieben.
  • Bootloader und Datenträgersignatur blockweise kopieren (die ersten 446 Bytes des Datenträgers):
    # dd if=/dev/sda of=/dev/sdc bs=446 count=1
    1+0 records in
    1+0 records out
    446 bytes (446 B) copied, 0.00403888 s, 110 kB/s
  • kleine “System Reserved”-Partition blockweise kopieren:
    # dd if=/dev/sda1 of=/dev/sdc1
    204800+0 records in
    204800+0 records out
    104857600 bytes (105 MB) copied, 2.02602 s, 51.8 MB/s
  • Windows-Partition (“C:”) blockweise kopieren:
    # dd if=/dev/sda2 of=/dev/sdc2
    234231808+0 records in
    234231808+0 records out
    119926685696 bytes (120 GB) copied, 2271.83 s, 52.8 MB/s
  • NTFS-Dateisystem auf /dev/sdc2 vergrößern:
    • GParted starten.
    • /dev/sdc2 rechtsklicken und Check wählen.
    • In der Ausgabe kann man verfolgen, dass ntfsresize --force --force /dev/sdc2 aufgerufen wird. Es wird der Größenunterschied zwischen Dateisystem und Partition aufgeführt: Current volume size: 119926682112 bytes (119927 MB) vs. Current device size: 512004284416 bytes (512005 MB). Am Ende steht ein Successfully resized NTFS on device '/dev/sdc2'. Da keine Daten verschoben werden müssen, dauert dieser Vorgang nur wenige Sekunden.

Bei den obig angegebenen Schritten ist zu beachten, dass ein Vertauschen/Verschreiben bei den Gerätepfaden (/dev/sd*) unverzeihlich ist. Bitte drei mal denken und kontrollieren, bevor Ihr auf Enter drückt. Ansonsten habt Ihr ja sicherlich eine Sicherung Eurer persönlichen Daten.

6) Rückbau des Datenträgers in das Produktivsystem (5 Minuten)

Der neue Datenträger mit der Windows-Installation wird in das Produktivsystem zurückgebaut. Per BIOS-Einstellung wird das Produktivsystem vom neuen Datenträger gestartet. Es sind keine weiteren Schritte erforderlich.

Fazit

Etwa 25 Minuten Arbeit für eine Betriebssystemmigration (+ Zeit für Datentransfer). Das ist schnell. Warum gewährleistet obige Strategie Zuverlässigkeit? Der durchgeführte Prozess stellt sicher, dass sich zwischen der alten und der neuen Situation nur zwei Kleinigkeiten geändert haben: die Bezeichnung des Datenträgers und die Größe des NTFS-Dateisystems in dem Windows (mehrheitlich) installiert ist. Aus Sicht des BIOS und aus Sicht des Betriebssystems ist der Rest des Gesamtsystems äquivalent zum vorherigen Zustand. Mit dieser Betrachtungsweise ist es leicht zu verstehen, dass die Migration mit hoher Wahrscheinlichkeit gelingt. Die präzise Kontrolle der Geschehnisse erleichtert auch eine etwaige Fehlersuche: gibt es nach Schritt 6 ein Problem, so muss es an einem der beiden geänderten Parameter liegen (in der Tat: UEFI Systeme mögen bedingt durch den Wechsel des Geräte-Namens bzw. der Geräte-ID einen weiteren kleinen Schritt erfordern).

Bei Eurem nächsten Wechsel des Systemdatenträgers solltet Ihr die Migration systematisch durchführen. Wenn Ihr den Prozess unter Kontrolle habt, dann gelingt er mit hoher Wahrscheinlichkeit. Traut lieber den quelloffenen Standard-Werkzeugen welche in professionellen Umgebungen eingesetzt werden als den großen Versprechungen von (kommerziell erhältlichen) Software-Assistenten für Privantanwender.

Hello SPF record.

Introduction

Say Google’s or Yahoo’s mail transfer agent (MTA) retrieves an email sent from an MTA running on the host with the (fictional) IP address 1.1.1.1. Say the sending MTA identifies itself as sending on behalf of important.com (within the EHLO/HELO handshake, or with a From address with the domain important.com). Then the receiving MTA (Google or Yahoo, or something else) usually queries the important.com domain via DNS for the so-called SPF (Sender Policy Framework) record. Using this SPF record the retrieving MTA can verify whether the owner of the domain important.com intended to send mail from 1.1.1.1 or not.

The SPF record might say that important.com does not send mails. Then the receiving MTA knows for sure that the incoming mail is invalid/fake/spam. Or this record says that important.com actually sends mail, but only from the (fictional) IP address 2.2.2.2. Then the retrieving MTA also knows for sure that something is wrong with the mail. Another possibility is that the record states that important.com actually sends mail from 1.1.1.1, in which case the incoming mail likely is intended by the owner of the domain important.com. This does not mean that this mail is not spam, but at least one can be quite sure that nobody crafted that mail on behalf of important.com.

That was a simplified description for why it is important to have a proper SPF record set for a certain domain in case one intends to send mail from this domain. In the next paragraphs I explain how I have set the SPF record for gehrcke.de and why.

SPF record for gehrcke.de

Domainfactory is the tech-c for gehrcke.de. However, the DNS entries for gehrcke.de are managed by Cloudflare (free, and quite comfortable). Today, I added an SPF record for gehrcke.de via Cloudflare’s web interface. The first important thing to take note of is that the SPF record actually is a TXT record. There once was a distinct SPF record proposed, but in RFC 7208 (which specifies SPF) we clearly read:

The SPF record is expressed as a single string of text found in the RDATA of a single DNS TXT resource record

and

SPF records MUST be published as a DNS TXT (type 16) Resource Record (RR) [RFC1035] only. Use of alternative DNS RR types was supported in SPF’s experimental phase but has been discontinued.

By now it should be clear that I had to add a new TXT record for gehrcke.de, with a special syntax, the SPF syntax. This syntax is specified here. What I need to express in the record is that mail sent on behalf of gehrcke.de is only sent by the machine with the IPv4 address 5.45.109.1 / or from the IPv6 subnet fe80::5054:8dff:fe9b:358d/64. This is the machine that I know should send mail. No other people / machines should send mail on behalf of my domain. The corresponding record is:

v=spf1 ip4:5.45.109.1 ip6:fe80::5054:8dff:fe9b:358d/64 -all

The prefix v=spf1 declares the SPF version, the ip4 and ip6 markers declare that mail from a certain address or subnet are valid. -all expresses that mail from all other hosts is disallowed. The fact that I do not “allow” others to send mail on behalf of gehrcke.de is just my declaration of intent. A retrieving MTA is not required to respect my SPF record. However, larger mail providers should respect it and at least classify a mail that does not pass the SPF test as spam, if not reject it right away.

Whenever I switch machines or IP addresses, I need to remember to update this record. This is a minor disadvantage. To circumvent this, in many cases a short

v=spf1 a -all

would be enough. It expresses that mail sent from the IP address corresponding to the DNS A record of the domain is allowed. In my case, however, this does not work since the A record points to Cloudflare’s cache rather than directly to my machine.

Testing the record

There are many web-based tools for checking whether a certain domain has an SPF record set and whether its syntax is valid. Particularly useful is, for instance, mxtoolbox.com/SuperTool.aspx. However, a real full-system validation obviously requires you to send a mail to an external service that resembles a serious mail provider and debugs the communication for you, meaning that it informs you about all externally visible details of your mail setup, including the SPF record. A great verification tool that does exactly this is provided by Port25. On the command line I used my local MTA (Exim4) to send mail to their service, instructing them to return the corresponding report to may Googlemail address:

$ echo "This is a test." | mail -s Testing check-auth-jgehrcke=googlemail.com@verifier.port25.com

The following is an excerpt of the test result, indicating that the actual mail sender matches the data provided in the SPF record:

HELO hostname:  gurke2.gehrcke.de
Source IP:      5.45.109.1
mail-from:      user@gehrcke.de
 
----------------------------------------------------------
SPF check details:
----------------------------------------------------------
Result:         pass 
ID(s) verified: smtp.mailfrom=user@gehrcke.de
DNS record(s):
    gehrcke.de. SPF (no records)
    gehrcke.de. 300 IN TXT "v=spf1 ip4:5.45.109.1 ip6:fe80::5054:8dff:fe9b:358d/64 -all"

Great.