Steve's Blog

Secure DNS with bind and DoT

Starting with BIND 9.19, you can now set up DNS over TLS in the forwarders option.

You can use this in Fedora now by installing the bind9-next packages instead of bind.

Configuring this is quite simple, the example below uses Google, Quad9 and Cloudflare as upstream DNS servers:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
tls cloudflare-tls { remote-hostname "one.one.one.one"; };
tls quad9-tls { remote-hostname "dns.quad9.net"; };
tls google-tls { remote-hostname "dns.google"; };
options {
    ...
    forwarders port 853 {
        1.1.1.1 tls cloudflare-tls;
        1.0.0.1 tls cloudflare-tls;
        2606:4700:4700::1111 tls cloudflare-tls;
        2606:4700:4700::1001 tls cloudflare-tls;

        9.9.9.9 tls quad9-tls;
        149.112.112.112 tls quad9-tls;
        2620:fe::fe tls quad9-tls;
        2620:fe::9 tls quad9-tls;

        8.8.8.8 tls google-tls;
        8.8.4.4 tls google-tls;
        2001:4860:4860::8844 tls google-tls;
        2001:4860:4860::8888 tls google-tls;
    };
};

Customise the above however you like to disable IPv6 servers, or a certain upstream provider.

Keep in mind that all traffic for upstream DNS will now go to port 853 on the target upstream.

Simple and cheap Stratum 1 NTP with GPS

The Linus Tech Tips channel ShortCircuit recently did a video on an NTP time source card. Video here for reference:

It’s really nice to have your own clock source, but when you start digging into the price of those units, saying you might not get change from $13,000 isn’t a lie. Of course, that’s for the highest end cards - but do we really need that for a small user? What if you could get most of the way there for about $50 USD installed?

Good news - you can.

The idea is to use one of these GPS recievers with a little bit of level conversion and hook it straight into your serial port. It’s exactly the same method as when I did this about 8 years ago - however the tooling has changed a little since then - so lets revisit this topic with some modern tools.

We’ll be using chronyd as our NTP time server - as its pretty much the default everywhere these days, along with gpsd and its tools to configure the module properly.

What you’ll need:

Looking at the GPS module, you’ll see it’s pretty straight forward. TX / RX for serial data, PPS for the pulse, and power.

GPS Module

Here’s a quick picture of my plug wiring. In my setup, my RS232 port allows me to inject 5vDC on pin 9 of the DB9. You probably won’t have this, so you’ll need to supply 5vDC to the VCC pad on the converter - which also connects to the GPS Vcc - and one of the GND pads on the board. It’s important that the serial port ground, the power supply ground, and the level converters grounds are all connected.

Level Converter Side 1
Level Converter Side 2

On the hardware side, that’s pretty much it - so now, lets set up the software.

I installed mine on a Proxmox server - so everything here is based on a Debian install. These tools are generic, so search for them on your distro.

Firstly, install the required packages:

1
2
# apt-get update
# apt-get install gpsd gpsd-tools setserial chrony

Now, to configure gpsd, edit the file /etc/default/gpsd, and make its contents as below. Subsitute your serial port instead of /dev/ttyS0.

1
2
3
4
5
6
7
8
9
10
# Devices gpsd should collect to at boot time.
# They need to be read/writeable, either by user gpsd or the group dialout.
DEVICES=""

# Other options you want to pass to gpsd
GPSD_OPTIONS="-n"
OPTIONS="-s 38400 -F /run/gpsd.sock /dev/ttyS0"

# Automatically hot add/remove USB GPS devices via gpsdctl
USBAUTO="false"

Now we’re going to want to edit the gpsd systemd service to add some commands to initialise the GPS module on startup.

Do this via systemctl edit gpsd and add in the following - again, alter your serial port as required:

1
2
3
4
5
6
7
[Service]
ExecStartPre=/usr/bin/setserial /dev/ttyS0 low_latency
ExecStartPre=/usr/bin/ubxtool -f /dev/ttyS0 -P 18 -s 9600 -S 38400
ExecStartPre=/usr/bin/ubxtool -f /dev/ttyS0 -P 18 -s 38400 -p MODEL,2
ExecStartPre=/usr/bin/ubxtool -f /dev/ttyS0 -P 18 -s 38400 -e BINARY
ExecStartPre=/usr/bin/ubxtool -f /dev/ttyS0 -P 18 -s 38400 -d NMEA
ExecStartPre=/usr/bin/ubxtool -f /dev/ttyS0 -P 18 -s 38400 -e PPS

Finally, we set up chronyd to use both the GPS and PPS output. Edit /etc/chrony/chrony.conf and add this at the bottom:

1
2
3
4
5
refclock SHM 0 refid GPS offset 0.600 delay 0.2
refclock SHM 1 refid PPS offset 0.0 delay 0.0

server pool.ntp.org iburst
allow 10.0.0.0/24

In this config, we deliberately add an error to the GPS lines. This is because the NMEA data can be quite regular, and in some cases people have seen it being preferred as a source over the PPS. Inducing an error here will ensure that we always use the PPS source.

The last two lines allow your network to use the chronyd instance as an NTP source and sets an external reference to start against.

Now to configure these services to start on boot, and start them now:

1
2
# systemctl enable gpsd chronyd
# systemctl restart gpsd chronyd

If you then watch the logs in journald, you’ll see something like this:

1
2
3
4
5
6
7
systemd[1]: Starting chrony.service - chrony, an NTP client/server...
chronyd[1084]: chronyd version 4.3 starting (+CMDMON +NTP +REFCLOCK +RTC +PRIVDROP +SCFILTER +SIGND +ASYNCDNS +NTS +SECHASH +IPV6 -DEBUG)
chronyd[1084]: Frequency -12.140 +/- 0.070 ppm read from /var/lib/chrony/chrony.drift
chronyd[1084]: Using right/UTC timezone to obtain leap second data
chronyd[1084]: Loaded seccomp filter (level 1)
systemd[1]: Started chrony.service - chrony, an NTP client/server.
chronyd[1084]: Selected source PPS

I then use this command to watch what’s going on: watch "chronyc tracking; echo; chronyc sources; echo; chronyc sourcestats"

You can check the accuracy in this by looking at this bit:

1
2
3
4
Name/IP Address            NP  NR  Span  Frequency  Freq Skew  Offset  Std Dev
==============================================================================
GPS                         7   3    90    +19.721     89.013   -382ms   664us
PPS                        64  31  1001     -0.000      0.071   -147ns    48us

Job done. Enjoy your very cheap stratum 1 NTP server.

Caching system updates for the home lab

If you’re like me, you’ve got a home lab with a dozen or so virtual machines doing all sorts of things - and each of them are pulling down updates from somewhere on the internet.

What if you could have a single endpoint for all VMs to reference? That way, updates that are common would be distributed to all systems at LAN speeds after the first download.

Introducing - mod_cache for Apache :)

Assuming you’re already running Apache somewhere, you can start mapping part of the local path structure to a remote endpoint.

Drop the following into /etc/httpd/conf.d/mod_cache.conf:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
CacheEnable	            	disk /fedora
CacheRoot	            	/var/cache/httpd/fedora
CacheMaxFileSize        	524288000
CacheDefaultExpire      	14400
CacheDetailHeader       	on

# common caching directives
CacheQuickHandler       	off
CacheLock	            	on
CacheLockPath	        	/tmp/mod_cache-lock
CacheLockMaxAge	        	5
CacheHeader	            	On

# cache control
#CacheIgnoreNoLastMod   	On
#CacheIgnoreCacheControl	On
   
# unset headers from upstream server
Header unset Expires
Header unset Cache-Control
Header unset Pragma

ProxyRequests	        	Off
ProxyPass   	        	/fedora http://dl.fedoraproject.org/pub/fedora
ProxyPassReverse        	/fedora http://dl.fedoraproject.org/pub/fedora

UseCanonicalName        	On

When in use, this will map http://my.apache.host/fedora to the Fedora mirror, and cache all responses and downloaded files.

The cache won’t automatically clean itself though - so we need a systemd service to clean things up over time. Create the file /etc/systemd/system/http-cache-clean.service as follows:

1
2
3
4
5
6
7
8
9
10
[Unit]
Description=Apache cache cleaner
After=network-online.target

[Service]
Type=forking
ExecStart=/usr/sbin/htcacheclean -d 60 -i -l 5G -p /var/cache/httpd/fedora/

[Install]
WantedBy=multi-user.target

This will limit the cache size to 5Gb and remove the oldest files first.

There is one gotcha when using this with Fedoras updates and dnf - zchunk. I believe this is because mod_cache doesn’t work on partial content requests - which is how zchunk functions.

To get around this, we can disable zchunk in the DNF configuration file /etc/dnf/dnf.conf. I also disable deltarpm - as its quicker to download the file from the LAN cache than it is to rebuild a drpm update.

1
2
3
4
5
6
7
8
9
10
[main]
gpgcheck=True
installonly_limit=3
clean_requirements_on_remove=True
best=False
skip_if_unavailable=True
max_parallel_downloads=10
fastestmirror=True
zchunk=False
deltarpm=0

We can then point the yum repo file to the local apache server - for example, part of /etc/yum.repos.d/fedora-updates.repo:

1
2
3
4
5
6
[updates]
name=Fedora $releasever - $basearch - Updates
#baseurl=http://download.example/pub/fedora/linux/updates/$releasever/Everything/$basearch/
#metalink=https://mirrors.fedoraproject.org/metalink?repo=updates-released-f$releasever&arch=$basearch
baseurl=http://my.apache.host/fedora/linux/updates/$releasever/Everything/$basearch/
enabled=1

SPDIF Optical Keepalive with Pipewire

For years, I’ve run a set of Logitech Z-5500 speakers into an optical port on my PC. It gives good quality 5.1 audio, and supports AC3 + DTS digital passthrough as well as 44, 48, and 96khz bitrates.

The problem is, the speakers go into a ‘sleep’ mode where it takes nearly a second to bring the amp back online to play audio - so notification sounds are often not played at all.

To correct this, in the past, I’ve run a simple systemd service using sox to output a sine wave that is below the audible level like so: /usr/bin/play -q -n -c2 synth sin gain -95

Now however, we can do this directly within pipewire itself.

Firstly, we need to identify the output device using pw-top. Play some audio, and look for which sink it is being played on - eg:

1
2
3
4
5
6
7
8
9
10
11
12
13
S   ID  QUANT   RATE    WAIT    BUSY   W/Q   B/Q  ERR FORMAT           NAME                                                                                                                                                                   
S   28      0	   0    ---     ---   ---   ---     0                  Dummy-Driver
S   29      0	   0    ---     ---   ---   ---     0                  Freewheel-Driver
S   36      0	   0    ---     ---   ---   ---     0                  Midi-Bridge
S   42      0	   0    ---     ---   ---   ---     0                  alsa_output.usb-Kingston_HyperX_Cloud_Stinger_Core_Wireless___7.1_000000000000-00.analog-stereo
S   49      0	   0    ---     ---   ---   ---     0                  alsa_input.usb-Kingston_HyperX_Cloud_Stinger_Core_Wireless___7.1_000000000000-00.mono-fallback
R   40   1024  48000  32.3us   4.3us  0.00  0.00    0    S16LE 2 48000 alsa_output.usb-Generic_USB_Audio-00.HiFi_5_1__hw_Audio_3__sink
R  106   1024  48000  20.5us   5.1us  0.00  0.00    0    F32LE 2 48000  + Brave
S   50      0	   0    ---     ---   ---   ---     0                  alsa_output.usb-Generic_USB_Audio-00.HiFi_5_1__hw_Audio_1__sink
S   51      0	   0    ---     ---   ---   ---     0                  alsa_output.usb-Generic_USB_Audio-00.HiFi_5_1__hw_Audio__sink
S   52      0	   0    ---     ---   ---   ---     0                  alsa_input.usb-Generic_USB_Audio-00.HiFi_5_1__hw_Audio_2__source
S   53      0	   0    ---     ---   ---   ---     0                  alsa_input.usb-Generic_USB_Audio-00.HiFi_5_1__hw_Audio_1__source
S   54      0	   0    ---     ---   ---   ---     0                  alsa_output.pci-0000_2f_00.1.hdmi-stereo

In my case, the audio device is alsa_output.usb-Generic_USB_Audio-00.HiFi_5_1__hw_Audio_3__sink.

Now we create a file at ~/.config/wireplumber/main.lua.d/spdif-noise.lua with:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
rule = {
  matches = {
    {
      { "node.name", "matches", "alsa_output.usb-Generic_USB_Audio-00.HiFi_5_1__hw_Audio_3__sink" }
    },
  },
  apply_properties = {
    ["dither.noise"] = 2,
    ["node.pause-on-idle"] = false,
    ["session.suspend-timeout-seconds"] = 0
  }
}

table.insert(alsa_monitor.rules,rule)

You’ll need to swap the name

Restart pipewire now: systemctl --user restart pipewire.service.

Now, when your first sound plays, pipewire will continue to output sub-audible noise to keep everything alive - which is a much better solution than using sox!

Training spam with doveadm

A while ago, I posted about training SpamAssassin Bayes filter with Proxmox Mail Gateway. That’s really easy when you’re using Maildir - as each email message is its own file.

At this point, we could easily just cat out a file and treat email in folders as files and ignore the fact they were part of an imap mailbox. However, what happens if you use something other than Maildir - like the newer mailbox formats? We can’t use the same approach, as each email is likely not just a file anymore.

For example, dbox is Dovecot’s own high-performance mailbox format.

If we use mdbox, we can no longer open a single message per file, nor can we tell what folders are what from the on disk layout. So we have to get smarter.

Using doveadm, we can search for messages in a mailbox, and fetch them to feed into our previously configured script and feed them into PMG as before. The main advantage is that this will work with any mail storage backend.

This simple bash script will go through all users Spam or INBOX/Spam folders and fetch each one, feed it into the learning system, and then remove it from the users mailbox.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
	#!/bin/bash
	MAILFILTER=my.pmg.install.example.com
	shopt -s nullglob

	doveadm search -A mailbox Spam OR mailbox INBOX/Spam | while read user guid uid; do
		doveadm fetch -u $user text mailbox-guid $guid uid $uid | tail -n+2 > /tmp/spam.$guid.$uid
		cat /tmp/spam.$guid.$uid | ssh root@$MAILFILTER report
			if [ $? != 0 ]; then
				echo "Error running sa-learn. Aborting."
				exit 1
			fi
			rm -f /tmp/spam.$guid.$uid
			doveadm expunge -u $user mailbox-guid $guid uid $uid
		done

Use it with the scripts / general configuration from the previous article, and this should be able to be used across all mail storage methods supported by Dovecot.

Cron it to run every 5 minutes or so, and you’re done! Nice and easy.