[{"content":"Suppose you want to run someone else\u0026rsquo;s blog. Not steal it, exactly. Just quietly continue it under a name that is legally distinct enough that nobody sends an email. Keep the posts, keep the opinions about inotify, keep the layout override you never finished. Only the name on the door changes.\nThis is a short guide. The someone else, in my case, is me.\nStep 1: pick a name that is close, but not too close The new name has to clear a few bars. It must be pronounceable by a human who is not currently looking at it. It must survive a domain registrar without a hyphen. It must not already be a Mastodon instance run by someone with more free time and more anger than you.\nI ran git grep -i mynacol, counted the hits, and started brainstorming replacements that shared enough letters to feel like the same lineage. After a satisfying amount of muttering, I landed on Macrogol. Six letters in common with the original. Rolls off the tongue. Vaguely Latin, vaguely serious, the sort of word a small consultancy would have.\nI registered the domain. I updated hugo.toml. I updated baseURL, both title fields, both homeInfoParams, the author param, the README. I pushed. I felt clever for roughly forty minutes.\nStep 2: commit before you google, regret after A friend replied to my \u0026ldquo;new blog name, same great content\u0026rdquo; message with a single line:\nisn\u0026rsquo;t macrogol the laxative\nReader, it is. Macrogol is polyethylene glycol 3350. It is the stuff you dissolve in a large glass of water the night before a colonoscopy. Its entire professional purpose is to keep things moving.\nSo that is the brand now. A personal blog about Linux, self-hosting and privacy, named after an osmotic laxative. Honestly it is not the worst metaphor for a homelab: mostly water, works slowly, occasionally dramatic.\nStep 3: find everything you missed Renaming a blog is a lesson in how many places a name hides. The obvious ones go quickly. Then you notice the \u0026lt;title\u0026gt; in the generated index.xml, the alt text on the favicon, the OpenGraph tags, that one hardcoded string in a partial you copied from the theme two years ago.\nenableGitInfo = true is the real fun. Every historical commit still renders the old name in the footer\u0026rsquo;s \u0026ldquo;last modified\u0026rdquo; line, linking back to a repo path that now redirects. You can rebrand the present. The git log stays exactly as smug as it was.\nStep 4: tell people, or don\u0026rsquo;t There is a bold option here where you change nothing else and let RSS readers work it out themselves. One day the feed is one name, the next day it is another, same avatar, same posting schedule, no explanation. Let them wonder whether they subscribed to a person or inherited a franchise.\nI am taking the slightly more responsible path and writing this post instead.\nIn conclusion \u0026ldquo;Faking\u0026rdquo; your own blog under a new name turns out to be indistinguishable from simply renaming it. Same person, same archive, same half-finished draft about NixOS that will never ship. The only real change is that the branding is now, medically speaking, a stool softener.\nIf you are reading this in a feed reader that still says the old name: don\u0026rsquo;t worry about it. Everything is fine. Everything is moving along nicely.\n","permalink":"https://blog.macrogol.xyz/en/faking-your-own-blog/","summary":"\u003cp\u003eSuppose you want to run someone else\u0026rsquo;s blog.\nNot steal it, exactly.\nJust quietly continue it under a name that is legally distinct enough that nobody sends an email.\nKeep the posts, keep the opinions about \u003ccode\u003einotify\u003c/code\u003e, keep the layout override you never finished.\nOnly the name on the door changes.\u003c/p\u003e\n\u003cp\u003eThis is a short guide.\nThe someone else, in my case, is me.\u003c/p\u003e\n\u003ch2 id=\"step-1-pick-a-name-that-is-close-but-not-too-close\"\u003eStep 1: pick a name that is close, but not too close\u003c/h2\u003e\n\u003cp\u003eThe new name has to clear a few bars.\nIt must be pronounceable by a human who is not currently looking at it.\nIt must survive a domain registrar without a hyphen.\nIt must not already be a Mastodon instance run by someone with more free time and more anger than you.\u003c/p\u003e","title":"How to Fake Someone Else's Blog Under a Different Name"},{"content":"Earlier this year I brought an old laptop to my home my grandfather once gave. It is a relict from roughly 1999, with which I\u0026rsquo;ve already played around some nine years ago.\nPrior to 38C3 I had the idea to bring it to the congress so people could mess around with it. In the end, I was the one messing around with it, fighting to get some operating systems installed on this poor thing.\nHardware The laptop I\u0026rsquo;m talking about is a Fujitsu-Siemens Lifebook S4510 with BIOS version 1.36 released at 1999-11-30. The hostnamectl status output is expectedly really hilarious (the firmware age is originally printed in yellow):\nFirmware Version: Version 1.36 Firmware Date: Tue 1999-11-30 Firmware Age: 25y 1month 5d CPU This device has a Intel(R) Pentium(R) III (Coppermine) processor with astonishing 0.4GHz single core. No multithreading. According to Wikipedia, this processor was produced with a 180 nm process node. It has 32 KiB of L1 cache and a 256KiB L2 cache, not bad for its time.\nRAM This thing boasts 256 MiB of SDRAM running at 100 MHz. According to lshw, it even has an empty second slot, but I don\u0026rsquo;t have any sticks to insert.\nBattery There is an internal Lithium-ion battery with originally 28 Wh of capacity. I believe some models even had the room for two batteries for easy switching. Unfortunately, the driver cannot give me details on the remaining capacity, but after this time it is expectedly bad. Some minutes and it\u0026rsquo;s completely empty, giving a last beep before turning forcefully off.\nI/O This thing has versatile I/O functionality, but not what one expects from current computers. It has internally:\nIDE for the hard drive (40 GB from Fujitsu) An IDE-like connector for CD drives. I have a CD-ROM drive (not DVD) and a SuperDisk drive with one 120 MB floppy for this slot. PCMCIA. I have adapters for a serial (RS-232) connection, SmartMedia memory cards and CompactFlash storage devices. I heavily use the one 512 MB CompactFlash card I have as swap storage for better random I/O performance. I also know about WLAN cards and cards offering more USB ports for PCMCIA. The external connectors are:\nVGA USB 1.1 (max. 12 Mbit/s) Ethernet (100 Mbit/s) Dialup modem port Infrared transciever A mini RS-232 connector? 3.5 mm headphone, microphone and the third audio input jacks A passive docking station with a probably proprietary connector extends the I/O with:\nPS/2 ports for keyboard and mouse parallel printer connector full-size RS-232 connector mini external floppy drive connector, for which I actually have the fitting external floppy drive Software I had played around with this computer some nine years ago. Back then I installed antiX on it, a Debian fork particularly suited for old devices, without systemd. I was pleasantly surprised that this old installation was working just fine.\nFor the congress, I thought it would be funny to install Windows 98 or XP on this system. Windows 98 was the preinstalled system back then, at least a license sticker is still glued at the bottom of this machine. Unfortunately, I forgot my CD collection at home, resorting to other means starting an operating system. Not easy on this old machine.\nBooting from USB The BIOS cannot boot from USB, the boot menu only offers the hard drive (on which we want to install a system on), the CD or SuperDisk, the external floppy as boot media, and finally network boot. The floppy and SuperDisk fall short on storage size, and CDs were not available to me at that time. And I had no intent to setup network boot. What I then tried to use was Plop, a custom boot manager I already found those nine years ago. It\u0026rsquo;s written in pure assembly and really tiny, fitting on a floppy or in the first MB of a hard drive (MBR plus some empty space). It can boot from almost anywhere and start almost anything, as it has its own CD and USB drivers. This finally allowed me to boot some installation media, namely Linux and Windows.\nWindows Despite extensive tries, we were not able to get Windows 98 SE or Windows XP to boot on this machine. Using Ventoy on USB to boot the ISO got me into the Windows XP setup, but once the actual setup would start, a bluescreen with the feared STOP: 0x0[...]07B error code appeared, which is commonly induced by using AHCI instead of IDE. This can be ruled out in our case, at least the hard drive is for sure connected with IDE. But to also rule out the USB stick with the Plop bootloader driver, we installed both Windows 98 and XP in a virtual machine, extracted the disk image (or rather the main partition) and dd\u0026lsquo;ed it on the laptop. This led to a frame of the Windows XP boot animation, before the same bluescreen error message appeared here as well. Somehow Windows XP doesn\u0026rsquo;t like my hard drive.\nBack home, I tried to boot the installers from CD and was even less successful. Nothing really booted from CD. After some back and forth I used my existing Linux to just dd the contents – directly leading to I/O errors. I checked with multiple CD\u0026rsquo;s, but the issue remained. Therefore I guess the drive or the connector to the motherboard has some issues. Afterwards, I also tested the SuperDisk and also got I/O errors. But then normal floppies in the SuperDisk worked fine, so maybe the CD drive and the one SuperDisk floppy I have are broken.\nI then tried again to create a bootable Windows XP installer USB stick. This topic is surprisingly hard, as Windows XP was not designed to be bootable from USB. This led to the hilarious error message Setup cannot find the End User License Agreement. Rufus, my go-to bootable USB creator on Windows was insofar unavailable, as my local Windows installation on my desktop just wouldn\u0026rsquo;t boot after being back home. It turned out that the issue here was a bad SATA cable, leading to I/O errors and somehow a corrupted Windows bootloader (something with BCD, which the Windows installer wasn\u0026rsquo;t able to fix, even with some shell fu). So I first tried Ventoy (again), later on also woeusb.\nAfter reinstalling Windows on my main desktop PC, I also tried Rufus (versions 2, 3 and 4, as version 2 was the latest version officially supporting Windows XP). I still had issues, so I also tried the alternatives linked at the old version of Rufus\u0026rsquo;s FAQ, UNetbootin and Universal USB installer. UNetbootin (at least the newest version) seemingly has no explicit support for Windows XP, it just tries to unpack the ISO with 7zip and do some other stuff, but the result was not bootable IIRC. I\u0026rsquo;m not sure how Universal USB Installer went, but then I also found a tool that used GRUB4DOS, which apparently made the Windows XP installer bootable.\nIn the end, I used that one to try installing Windows XP on another old desktop PC I have. That worked fine, confirming that the USB stick is actually bootable and installable. However, the old laptop failed to fully boot the setup most of the time, and always stopping before the actual installation could start. This feels like an issue dropping the power to the USB port to me. But for now no Windows for this old laptop.\nDuring my endeavours with Windows XP, I also ruled out that my copy of the ISO was somehow broken. I tried two copies I had stored, an original ISO with Service Pack 3, and a bundled and modified version from 2015 with all updates and some other tweaks. An official version could also be downloaded from massgrave.dev, but a version on archive.org with all updates (up until 2020) and pre-activated, but otherwise untouched caught my eye. I installed that version on my desktop and it went great – although I skipped the installation of the required LAN driver, so I had no internet and didn\u0026rsquo;t do much with the installation.\nLinux The i686 support in mainstream Linux distributions is nowadays quite slim. NixOS removed official support at the start of 2024, removing the ISO image and stopping to build almost all packages. Arch Linux discontinued 32-bit x86 in 2017 already. Linux Mint has no i686 support since version 20, except for the Debian edition. But that didn\u0026rsquo;t boot, probably due to the small RAM. Fedora also doesn\u0026rsquo;t offer a i686 version anymore. Towards the end of year, even Debian announced partially discontinuing i686 support by removing installer images for the next release.\nThat is generally a healthy sign, as 64 bit variants provide a multitude of advantages in supported RAM, security mitigations and performance. For old devices it remains to be seen if such support can be uphold over time.\nDebian Booting the Debian 12 netinstaller ISO from USB worked as expected and Debian was helpful in noting the lack of memory I have. Contrary to this scary warning message, a further dialogue gave me a list of modules and functionality to load. By only loading support tools for required filesystems, the default network drivers and the likes, I never had issues with this installer. Good job, Debian, for handling such edge cases in 2025.\nNixOS Unsatisfied with the Windows progress, we had the crazy idea to build NixOS for i686-linux during congress. At first we wanted to create a i686 ISO image as installer, to be able to boot it and from there and do nixos-generate-config. The first compilation errors could be avoided by just removing the respective packages from the ISO image. We finally failed due to a syslinux compilation error. Syslinux is the mandatory bootloader for BIOS (non-UEFI) systems, and replacing that is not easily possible.\nAfter talking to the NixOS assembly at the 38C3, we wanted to create a raw disk/partition image to dd onto the disk and to be able to later install grub as bootloader. Some packages still had compilation errors, which led us to abandon the image building for now.\nBuilding on the Debian installation, I could instead install nix with apt and run hello after the laptop itself compiled it, before leaving the congress. Some days later, I picked up this work by using my desktop pc to build nixos-install-tools, transmit it with the Debian ssh daemon and be able to execute nixos-generate-config on the laptop. I then used this config to build a full base system for i686, which surprisingly went smoothly after the compilation of the kernel and ghc was done. This was also transmitted with nix-copy-closure and a manual call to switch-to-configuration boot with the necessary lustrate setup prepared beforehand meant that on reboot I was running NixOS on this old laptop.\nUnfortunately, I tried the install without enabling openssh, which made it very painful to get a running ssh server back. Just changing the config on the laptop was quickly ruled out, especially because the swap was not (correctly) set up. In the end, I opted to build a static i686 openssh package, which is fortunately relatively easy: nix-build -A pkgsStatic.openssh --system i686-linux. However, how do I get this over to the laptop without ssh? I copied the path onto a USB stick and tried to execute sshd directly. That failed, as openssh was expecting the path to be placed inside of the nix store.\nThat led me to my most deranged move yet: I remounted the nix store read-write and copied this folder manually at the right place. Then I had to generate a ssh host key, place a configuration file at /etc/ssh/sshd_config and add a sshd user and group. After that, sshd was finally running and I could nixos-rebuild into the laptop again. My next config iteration added the ssh server, but on reboot I unfortunately had to see that sshd was failing because its dependency, nsncd, was also failing. I was noticing this failure previously, but didn\u0026rsquo;t realize that ssh depends on it, leading to a never-ending boot. Booting with sshd masked (because I had no previous generation in the bootloader?) finally brought me back into the system, and with a second static sshd invocation, I was finally fully back.\nRust on i686 The nsncd issue was an illegal instruction error. After some research I learned it is caused by Rust not supporting old i686 CPUs without SSE4. My laptop barely has the mmx and sse extensions, Rust applications will not run on it until Rust would add support on their side. For now, switching nsncd back to nscd avoids this Rust service (services.nscd.enableNsncd = false). The same is true for the switch-to-configuration program (system.switch.enableNg = false) and for the shutdown ramfs functionality (systemd.shutdownRamfs.enable = false). Now NixOS always warns me that the old switch-to-configuration program will be removed in NixOS 25.05, probably preventing me to ever update to this version. NixOS 24.11 seems to be the last runnable version on Pentium III CPUs.\nPCMCIA on NixOS Then I headed to the last error I faced. I had inserted the PCMCIA card into the NixOS config as swap storage, but that wasn\u0026rsquo;t available. After too much time, I found hardware.pcmcia.enable, which promptly led to a compilation error. In response, I fixed two small issues and created PRs (1, 2) to fix them also in nixpkgs. After rebuilding and rebooting, the CompactFlash storage was just available, and now the swap is running as intended.\nThat concludes my journey on this laptop so far, I learned some things on the way and even was able to fix at least some of the NixOS issues I faced. Some i686 related issues remain, but it is also not an officially supported system.\n","permalink":"https://blog.macrogol.xyz/en/nixos-on-fossils/","summary":"\u003cp\u003eEarlier this year I brought an old laptop to my home my grandfather once gave.\nIt is a relict from roughly 1999, with which I\u0026rsquo;ve already played around some nine years ago.\u003c/p\u003e\n\u003cp\u003ePrior to \u003ca href=\"https://events.ccc.de/congress/2024/infos/startpage.html\"\u003e38C3\u003c/a\u003e I had the idea to bring it to the congress so people could mess around with it.\nIn the end, I was the one messing around with it, fighting to get some operating systems installed on this poor thing.\u003c/p\u003e","title":"Laptop archeology – or – how to install NixOS 24.11 on a 25 year old laptop"},{"content":"Since some weeks I got a strange error message from the Nextcloud client on startup: Changes in synchronized folders could not be tracked reliably. At first, I ignored it, but I noticed Nextcloud wouldn\u0026rsquo;t sync local changes reliably to the server. After I lost a git commit (yes, I sync some git repos over Nextcloud), I took the issue seriously.\nI knew this was related to a Linux feature called inotify. With it, you can monitor files or directories and get notified by the kernel whenever the selected events happen on those elements. I searched the Nextcloud client and nixpkgs changelogs, but didn\u0026rsquo;t find anything related to my problem. Then I searched in the Nextcloud client source code and found the dialog I saw. However, that didn\u0026rsquo;t help me either.\nBy pure coincidence, I noticed a separate program (qt5ct) logging a failure relating inotify: inotify_add_watch(/home/user/.config/qt5ct) failed: (No space left on device) That was the information I needed. The issue was not rooted in Nextcloud itself, but rather a Linux kernel thing. Manually starting a inotifywait command let to the following insightful message:\nFailed to watch /tmp; upper limit on inotify watches reached! Please increase the amount of inotify watches allowed per user via `/proc/sys/fs/inotify/max_user_watches\u0026#39;. It\u0026rsquo;s a limit in the kernel that\u0026rsquo;s reached! And it even provides me the path to see and change it. At the same time I discovered inotify-info in nixpkgs. Its output was also very enlightening:\n------------------------------------------------------------------------------ INotify Limits: max_queued_events 16,384 max_user_instances 128 max_user_watches 227,675 ------------------------------------------------------------------------------ Pid Uid App Watches Instances 2001 1000 .nextcloud-wrapped 227,533 5 2712 1000 firefox 16 1 1993 1000 dbus-daemon 10 1 [...] ------------------------------------------------------------------------------ Total inotify Watches: 227633 Total inotify Instances: 21 ------------------------------------------------------------------------------ It confirms that the system has (almost) reached the max_user_watches limit. After raising the limit with sysctl fs.inotify.max_user_watches=1048576 and restarting the Nextcloud client, the message didn\u0026rsquo;t pop up again. I had solved my issue. The client had 295,041 watches after raising the limit. Finally, I preserved this change by setting boot.kernel.sysctl.\u0026quot;fs.inotify.max_user_watches\u0026quot; = 1048576; in my NixOS config. This change might lead to increased memory consumption, as each watch needs about 1KB. The 1,048,576 watches therefore might lead to about 1 GB of memory.\nDuring debugging, I looked at the output of inotify-info on my laptop. It had a limit half of the desktop (as it has half of the RAM), but the open watches was at about 95,000, so comfortably below the limit. I assume some git repos with a huge amount of small files I have on my desktop (e.g. nixpkgs) are the culprit of this limit. They are inside a synchronized folder, but exempt from synchronization due to poor performance. I know inotify-tools provides a recursive option. However, the syscalls don\u0026rsquo;t reflect that. Therefore, the client might prevent watching excluded subdirectories of synced folders. That\u0026rsquo;d help at least me staying below the inotify watch limit.\n","permalink":"https://blog.macrogol.xyz/en/nextcloud-inotify/","summary":"\u003cp\u003eSince some weeks I got a strange error message from the Nextcloud client on startup: \u003ccode\u003eChanges in synchronized folders could not be tracked reliably\u003c/code\u003e.\nAt first, I ignored it, but I noticed Nextcloud wouldn\u0026rsquo;t sync local changes reliably to the server.\nAfter I lost a git commit (yes, I sync some git repos over Nextcloud), I took the issue seriously.\u003c/p\u003e\n\u003cp\u003eI knew this was related to a Linux feature called \u003ccode\u003einotify\u003c/code\u003e.\nWith it, you can monitor files or directories and get notified by the kernel whenever the selected events happen on those elements.\nI searched the Nextcloud client and nixpkgs changelogs, but didn\u0026rsquo;t find anything related to my problem.\nThen I searched in the Nextcloud client source code and \u003ca href=\"https://github.com/nextcloud/desktop/blob/v3.12.2/src/gui/folder.cpp#L1495\"\u003efound\u003c/a\u003e the dialog I saw.\nHowever, that didn\u0026rsquo;t help me either.\u003c/p\u003e","title":"Nextcloud and inotify limits"},{"content":"Mail clients are notorious for sending information not really required for mail conversations. They leak their identity and version number, sometimes even the operating system in User-Agent and X-Mailer headers. They leak the locally set time zone in the Date header. And sometimes they leak the private IP addresses during mail sending.\nHere I document some settings to hide this information in my preferred mail clients. Note: Hiding this metadata might not be enough to fully disguise your choice of mail clients. The ordering of mail headers was not tackled, behavior with attachments varies equally high and other details disclose the mail client identity.\nThunderbird Thunderbird is a very popular mail client. While it prevents loading external resources by default, there are still improvements to be made.\nFirst off, Thunderbird by default sends a User-Agent header disclosing its identity. Up to Thunderbird 102, it sent a User-Agent similar to web browsers such as Firefox, e.g. User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:102.0) Gecko/20100101 Thunderbird/102.12.0. If you still run such version, you can suppress the User-Agent header by setting general.useragent.override in the advanced settings to an empty string.\nSince Thunderbird 115, there are two new preferences controlling the User-Agent. mailnews.headers.sendUserAgent controls if an User-Agent is sent at all. I recommend setting this to false. mailnews.headers.useMinimalUserAgent reduces the User-Agent to Thunderbird. Finally no version number, no operating system, no CPU architecture. On Arch Linux in a fresh Thunderbird profile, this setting is true by default.\nTo hide the time zone, set mail.sanitize_date_header to true. Surprisingly, this will not only change the time zone to UTC, but also nullify the seconds in the Date header. This way, time desynchronization of a couple of seconds is obfuscated. At the same time it might be a vector re-identifying Thunderbird amongst other mail clients.\nAdditionally, Thunderbird adds a Content-Language header by default. You can prevent sending it by setting mail.suppress_content_language to true.\nTelemetry Contrary to the other two mail clients in this article, Thunderbird contains telemetry. A great list tackling this problem is available in German at privacy-handbuch.de. Alternatively, you might get happy with the arkenfox user.js fork for Thunderbird from HorlogeSkynet.\nNeomutt Neomutt is a reasonable mail client regarding user privacy. Nonetheless, some improvements are possible.\nIf you don\u0026rsquo;t send a human-readable name alongside your email address, neomutt falls back to using your Unix username\u0026rsquo;s comment field. To prevent this, unset gecos_mask in your muttrc file.\nBy default, neomutt sends timestamps in the local time zone, e.g. Date: Fri, 08 Sep 2023 14:30:37 -0700. To always send in the UTC time zone, unset local_date_header in your muttrc file.\nNext, neomutt uses a random base32 string for the Message-ID header. To match the behavior of other popular mail clients such as Thunderbird, set my_hdr Message-ID: \u0026lt;`uuidgen -r`@example.com\u0026gt; in your muttrc. Remember to re-set this config option whenever you change the hostname for different accounts.\nFinally, neomutt uses the value in $hostname for the EHLO SMTP greeting. To hide a potential private IP address and match the others, set hostname = \u0026quot;localhost\u0026quot;.\nIn summary, set these options in your muttrc file:\nunset gecos_mask unset local_date_header my_hdr Message-ID: \u0026lt;`uuidgen -r`@example.com\u0026gt; set hostname = \u0026#34;localhost\u0026#34; FairEmail FairEmail is a modern, privacy respecting mail client for Android. It prevents loading external resources by default and uses IMAP IDLE to instantly notify on new messages without sending your credentials to another server (which is seemingly not standard anymore).\nTo enhance your privacy, you can avoid sending the fact that you are using FairEmail and how your time zone is currently set by going into its settings. Under the Privacy tab you find the following two settings: To also hide the local IP address in the SMTP EHLO message, you can set FairEmail to always use localhost. The setting can be found in the advanced identity settings per identity. For authenticated sending this string shouldn\u0026rsquo;t matter at all, but is recorded in the Received mail header. ","permalink":"https://blog.macrogol.xyz/en/mail-privacy/","summary":"\u003cp\u003eMail clients are notorious for sending information not really required for mail conversations.\nThey leak their identity and version number, sometimes even the operating system in \u003ccode\u003eUser-Agent\u003c/code\u003e and \u003ccode\u003eX-Mailer\u003c/code\u003e headers.\nThey leak the locally set time zone in the \u003ccode\u003eDate\u003c/code\u003e header.\nAnd sometimes they leak the private IP addresses during mail sending.\u003c/p\u003e\n\u003cp\u003eHere I document some settings to hide this information in my preferred mail clients.\n\u003cstrong\u003eNote:\u003c/strong\u003e Hiding this metadata might not be enough to fully disguise your choice of mail clients.\nThe ordering of mail headers was not tackled, behavior with attachments varies equally high and other details disclose the mail client identity.\u003c/p\u003e","title":"Increasing Privacy in Mail Clients"},{"content":"I\u0026rsquo;m using NixOS since over a year for most of my server stuff and I\u0026rsquo;m loving it. One of the missing services was my email setup, originally deployed on Debian 10, later upgraded to Debian 11. I followed a setup guide and was quite happy, tweaking it over the years to my needs. Roughly, the software stack consists of Postfix as the MTA (\u0026ldquo;SMTP server\u0026rdquo;), Dovecot as the MDA (\u0026ldquo;IMAP server\u0026rdquo;) and rspamd including its requirement, redis, for spam protection and SPF/DKIM/DMARC verification as well as DKIM signing for outgoing mail. I avoided a webmail service to keep things simple and I already use standard mail clients across my personal devices anyway.\nOriginally, I started hosting email myself to have a bigger part of the mail transmission pipeline under my control, and to force valid encryption against MITM attacks. That\u0026rsquo;s my alternative to using PGP. Recently, I wanted to move at least the mail storage to my personal server at home, so that no old mails lurk around in some hosted server indefinitely. So I had actually two incentives to change my mail server setup:\nMake the server config documented by moving to NixOS Move stored mail to my personal server for better privacy Unfortunately, hosting a mail server from home is difficult. Many ISPs don\u0026rsquo;t provide public IPv4 addresses to their customers by default, which is unfortunately still very important for email delivery. A lot of ISPs block port 25, needed for server-to-server connections, outright; not even receiving incoming mail is possible. Even some popular cloud hosting companies block port 25 by default, although you might have a better chance requesting unblocking than asking your ISP.\nMail server config After looking for ways to build a relatively standard mail server config for NixOS, I settled with the nixos-mailserver project. The easy configuration for a usual mail server setup is just a joy. The basic config for my server is the following:\nimports = [ (builtins.fetchTarball { url = \u0026#34;https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/${commit}/nixos-mailserver-${commit}.tar.gz\u0026#34;; sha256 = \u0026#34;sha256:0h35al73p15z9v8zb6hi5nq987sfl5wp4rm5c8947nlzlnsjl61x\u0026#34;; }) ]; mailserver = { enable = true; enableImap = false; enableImapSsl = true; enablePop3 = false; enablePop3Ssl = false; enableSubmission = false; enableSubmissionSsl = true; fqdn = \u0026#34;mail.mynacol.xyz\u0026#34;; domains = [ \u0026#34;mynacol.xyz\u0026#34; ]; certificateScheme = 3; dkimKeyBits = 3072; dmarcReporting.enable = false; loginAccounts = { \u0026#34;[user]@mynacol.xyz\u0026#34; = { hashedPasswordFile = \u0026#34;/var/lib/dovecot/passwords/[user]@mynacol.xyz\u0026#34;; aliases = [ \u0026#34;@mynacol.xyz\u0026#34; ]; }; }; mailDirectory = \u0026#34;/var/vmail\u0026#34;; messageSizeLimit = 104857600; # 100 MB localDnsResolver = true; }; But, of course, this deploys both the MTA (needing a public IP with unrestricted port 25 access) and MDA (having access to stored mails) on the same server. But I wanted to have the MDA (with the stored mails) on my private server at home!\nTunneling entire public IPs My solution is to provide unrestricted public IP addresses to my server at home via some tunneling. E-Mail is not using TLS directly by default, so tunneling solutions using TLS SNI headers to forward traffic is not possible here. But also forwarding all traffic on port 25 is not enough, as mail servers not only reply to client requests, but also initiate connections on their own when sending mail to other mail servers. Those connections have to use the right IP addresses as defined by your SPF policy, and, again, need to be able to send to port 25, which is strictly blocked by my home ISP. Therefore, I settled with a solution providing a virtual network interface: WireGuard.\nAfter some fiddling around, I\u0026rsquo;m able to \u0026ldquo;forward\u0026rdquo; public IPv4 and IPv6 addresses originally allocated to a vServer to my private server at home. I even managed to do that without NAT and other dirty tricks. The two important parts are to set the client IPs in WireGuard to the public IPs the server originally had (after removing them from the server), and to allow forwarding on the server side with some sysctl rules. This means you need multiple public IPs (can be IPv6) allocated to this vServer.\nServer config:\nboot.kernel.sysctl = { \u0026#34;net.ipv4.conf.default.forwarding\u0026#34; = 1; \u0026#34;net.ipv4.conf.all.forwarding\u0026#34; = 1; \u0026#34;net.ipv4.conf.default.proxy_arp\u0026#34; = 1; \u0026#34;net.ipv4.conf.all.proxy_arp\u0026#34; = 1; \u0026#34;net.ipv6.conf.default.forwarding\u0026#34; = 1; \u0026#34;net.ipv6.conf.all.forwarding\u0026#34; = 1; \u0026#34;net.ipv6.conf.default.proxy_ndp\u0026#34; = 1; \u0026#34;net.ipv6.conf.all.proxy_ndp\u0026#34; = 1; }; networking.wireguard.interfaces = { wg0 = { # Just some private IPs ips = [ \u0026#34;10.0.0.1/32\u0026#34; \u0026#34;fdfd:1234:fedc::1/128\u0026#34; ]; listenPort = 51800; privateKeyFile = \u0026#34;/wg0.privkey\u0026#34;; peers = [ { publicKey = \u0026#34;OV6Z2a2Q2Fgc2BOVAiGiPGzB5B9Ppzidm7qzTNVpKUc=\u0026#34;; allowedIPs = [ \u0026#34;[public IPv4 to forward]\u0026#34; \u0026#34;[public IPv6 to forward]\u0026#34; ]; } ]; }; }; Client config:\nnetworking.wireguard.interfaces = { wg0 = { ips = [ \u0026#34;[public IPv4 to forward]\u0026#34; \u0026#34;[public IPv6 to forward]\u0026#34; ]; #interfaceNamespace = \u0026#34;wg\u0026#34;; privateKeyFile = \u0026#34;/wg0.privkey\u0026#34;; peers = [ { publicKey = \u0026#34;Bk7rs7l3+3aOBgPYkHCGw1tP9aQZ4zcm9GH3kSsBQ3g=\u0026#34;; # Forward all the traffic via VPN. allowedIPs = [ \u0026#34;0.0.0.0/0\u0026#34; \u0026#34;::/0\u0026#34; ]; endpoint = \u0026#34;[server IP]\u0026#34;; } ]; }; }; After I deployed this config on my server, I noticed it didn\u0026rsquo;t fully work. Outgoing traffic was routed correctly, but I noticed with curl -4 https://wtfismyip.com/text that all my IPv4 traffic was routed through the vServer. Incoming traffic on the other hand was weird. This time, IPv4 traffic was just fine, but pings to the tunneled IPv6 weren\u0026rsquo;t answered. At first I assumed firewall settings or routing at the cloud hosting company was responsible, but nothing fixed it. So I started tshark, a CLI variant of Wireshark, to capture the traffic on my server. There I noticed that the server tried to answer connection requests, but with the wrong IPv6 address. That\u0026rsquo;s the reason responses weren\u0026rsquo;t coming through.\nUsing Network Namespaces in NixOS At that point I decided to isolate the network interfaces to avoid any routing (mis-)configurations altogether. This would also fix the default route for IPv4 traffic over the tunnel. While I could\u0026rsquo;ve used containers for this isolation and I believe the NixOS approach to containers is really nice, I didn\u0026rsquo;t want to mess with path mounts until the container saves the mails in a host directory. Instead, I used Network Namespaces directly.\nConveniently, the NixOS module for WireGuard interfaces added network namespaces support some time ago. Less conveniently, it requires an already existing second network namespace which you have to create manually. Fortunately, I found configs of other people doing exactly that.\nWhile trying this method I repeatedly stumbled over errors such as RTNETLINK answers: File exists. After some research I learned this happens if you try to create a network interface with an already taken name. And as my server already had a wg0 interface in the default namespace and the new config tries to create the new WireGuard interface first there before moving it in the other network namespace, I triggered this error message and thought my config was wrong. I rebooted my server (I could have removed the interface instead), and my config finally worked.\nIn the end I added a separate systemd service creating and deleting the namespace as required. The WireGuard interface service depends on the netns service and networking.wireguard.interfaces.wg0.interfaceNamespace automatically moves the wg0 interface into the second network namespace. Finally, I modified the postfix, dovecot, acme and kresd service configurations to exclusively have access to the second network namespace, and consequently to the mail-reserved public IPs. For redis I avoid IP sockets by using unix sockets instead.\nFull config:\nlet moveNs = { requires = [ \u0026#34;wireguard-wg0.service\u0026#34; ]; after = [ \u0026#34;wireguard-wg0.service\u0026#34; ]; serviceConfig.NetworkNamespacePath = \u0026#34;/var/run/netns/wg\u0026#34;; }; in { # Move Postfix in wg network namespace systemd.services.postfix = moveNs; # Move dovecot too, for the certificate they share the same domain name and therefore IP systemd.services.dovecot2 = moveNs; # Don\u0026#39;t use nginx, that might already be running in the default namespace security.acme.certs.\u0026#34;mail.mynacol.xyz\u0026#34;.listenHTTP = \u0026#34;:80\u0026#34;; security.acme.certs.\u0026#34;mail.mynacol.xyz\u0026#34;.webroot = null; # Fix certificate retrieval in network namespace systemd.services.\u0026#34;acme-mail.mynacol.xyz\u0026#34; = moveNs; # Fix DNS resolution systemd.services.\u0026#34;kresd@\u0026#34; = moveNs; # Use unix sockets for redis services.redis.servers.rspamd.port = 0; services.rspamd.locals.\u0026#34;redis.conf\u0026#34;.text = lib.mkForce \u0026#39;\u0026#39; servers = \u0026#34;${config.services.redis.servers.rspamd.unixSocket}\u0026#34;; \u0026#39;\u0026#39;; systemd.services.rspamd.serviceConfig.SupplementaryGroups = config.users.users.\u0026#34;${config.services.redis.servers.rspamd.user}\u0026#34;.group; # Add network namespace systemd.services.\u0026#34;netns@\u0026#34; = { description = \u0026#34;%I network namespace\u0026#34;; before = [ \u0026#34;network.target\u0026#34; ]; serviceConfig = { Type = \u0026#34;oneshot\u0026#34;; RemainAfterExit = true; ExecStart = \u0026#34;${pkgs.iproute2}/bin/ip netns add %I\u0026#34;; # Adds loopback IPs, else localhost connections are broken. ExecStartPost = \u0026#34;${pkgs.iproute2}/bin/ip netns exec %i ${pkgs.iproute2}/bin/ip link set dev lo up\u0026#34;; ExecStop = \u0026#34;${pkgs.iproute2}/bin/ip netns del %I\u0026#34;; }; }; networking.wireguard.interfaces.wg0.interfaceNamespace = \u0026#34;wg\u0026#34;; # Create wg namespace for wireguard connection systemd.services.wireguard-wg0 = { bindsTo = [ \u0026#34;netns@wg.service\u0026#34; ]; after = [ \u0026#34;netns@wg.service\u0026#34; ]; }; } ","permalink":"https://blog.macrogol.xyz/en/mail-server-at-home/","summary":"\u003cp\u003eI\u0026rsquo;m using \u003ca href=\"https://nixos.org/\"\u003eNixOS\u003c/a\u003e since over a year for most of my server stuff and I\u0026rsquo;m loving it.\nOne of the missing services was my email setup, originally deployed on Debian 10, later upgraded to Debian 11.\nI followed a \u003ca href=\"https://www.c0ffee.net/blog/mail-server-guide\"\u003esetup guide\u003c/a\u003e and was quite happy, tweaking it over the years to my needs.\nRoughly, the software stack consists of Postfix as the \u003ca href=\"https://en.wikipedia.org/wiki/Message_transfer_agent\"\u003eMTA\u003c/a\u003e (\u0026ldquo;SMTP server\u0026rdquo;), Dovecot as the \u003ca href=\"https://en.wikipedia.org/wiki/Message_delivery_agent\"\u003eMDA\u003c/a\u003e (\u0026ldquo;IMAP server\u0026rdquo;) and rspamd including its requirement, redis, for spam protection and SPF/DKIM/DMARC verification as well as DKIM signing for outgoing mail.\nI avoided a webmail service to keep things simple and I already use standard mail clients across my personal devices anyway.\u003c/p\u003e","title":"Hosting a Mail Server at Home"}]