Expanding Encrypted Swap and Adding ZRAM to Fix Memory Pressure Stalls

Despite having 32GB of RAM on my Debian Unstable system, I was experiencing random stalls and freezes triggered by memory pressure alerts from psi-notify. Sometimes the system would recover after a few seconds, but often it required a hard reboot. The solution was to expand my existing 4GB encrypted swapfile to 16GB and add ZRAM for compressed swap in RAM.

This builds on my previous guide for setting up encrypted swap on btrfs.

Expand Encrypted Swapfile

First, expand the existing encrypted swapfile from 4GB to 16GB:

# Turn off current swap
sudo swapoff /dev/mapper/swap
sudo systemctl stop systemd-cryptsetup@swap.service

# Remove old swap file
sudo rm /swap/swapfile

# Create new 16GB swap file
sudo btrfs filesystem mkswapfile --size 16g /swap/swapfile

# Reload and restart encrypted swap
sudo systemctl daemon-reload
sudo systemctl start systemd-cryptsetup@swap.service
sudo swapon /dev/mapper/swap

Add ZRAM for Compressed RAM Swap

Install ZRAM tools:

sudo apt update
sudo apt install -y zram-tools

Edit /etc/default/zramswap:

PERCENT=50       # 50% of RAM → ~16 GB compressed swap
ALGO=zstd        # fast and efficient
PRIORITY=100     # higher than disk swap

Enable and start the service:

sudo systemctl enable --now zramswap.service

Tune Swappiness

Lower swappiness so the kernel prefers RAM over swap:

sudo sysctl vm.swappiness=15
echo 'vm.swappiness=15' | sudo tee /etc/sysctl.d/99-swappiness.conf

Verify

Check that both swap devices are active:

sudo swapon --show
cat /proc/swaps

You should see both /dev/mapper/swap (encrypted disk swap) and /dev/zram0 (compressed RAM) listed as active swap devices.

captive-firefox: Solving Captive Portal Headaches with DNS over TLS

If you’re running systemd-resolved with DNS over TLS (like I detailed in my split DNS post), you’ve probably run into the same annoying problem I have: captive portals just don’t work properly.

The issue is straightforward but frustrating. Your system is configured to use secure DNS servers like Cloudflare (1.1.1.1) with DNS over TLS, which is great for security and privacy. But when you connect to that hotel Wi-Fi or coffee shop network, the captive portal can’t intercept your DNS queries to redirect you to their login page. Your requests bypass their DNS entirely, so you never see the portal.

The Manual Workaround (That Gets Old Fast)

The typical workaround involves giving the Wi-Fi network DNS priority and disabling DNS over TLS:

sudo resolvectl domain wlp0s20f3 "~." && sudo resolvectl dnsovertls wlp0s20f3 no
# Connect to captive portal
# Log in manually
# Revert changes
sudo resolvectl domain wlp0s20f3 "" && sudo resolvectl dnsovertls wlp0s20f3 yes

This works, but it’s annoying for two reasons: you have to remember to revert the changes, and it affects your entire system’s DNS behavior instead of just the browser you need for the captive portal.

Enter captive-firefox

I got tired of this dance and wrote a simple Bash script that handles captive portals elegantly. The idea is simple: launch Firefox in a sandbox that uses the Wi-Fi network’s DNS server directly, bypassing your system’s DNS configuration entirely.

Here’s what the script does:

  1. Auto-detects your Wi-Fi interface (or you can specify it)
  2. Extracts the DHCP-provided DNS server from NetworkManager
  3. Launches Firefox in a firejail sandbox with that specific DNS
  4. Uses a completely isolated profile – no access to your regular browsing data

The result? Firefox can see the captive portal while your system maintains its secure DNS configuration.

Zero Dependencies, Maximum Convenience

The script requires only standard Linux tools you probably already have:

  • Bash
  • firejail (for sandboxing)
  • nmcli (NetworkManager CLI)
  • iw (wireless tools)
  • Firefox

No Go binaries to compile, no complex dependencies. Just copy the script and run it.

Usage

Most of the time, it’s as simple as:

./captive-firefox.sh

The script will auto-detect everything and open Firefox pointing to the standard captive portal detection URL. For edge cases, you can specify the interface or target URL manually.

I’ve also included a .desktop file so you can launch it from your application menu when needed.

Security Considerations

The sandboxed Firefox instance:

  • Can’t access your real Firefox profile or data
  • Uses only the captive portal’s DNS (isolated from your secure setup)
  • Runs in a firejail container for additional isolation
  • Automatically cleans up when closed

Your main system’s DNS configuration remains untouched throughout the process.

Get It

The script is available on my GitHub: captive-firefox

Finally, a civilized way to deal with captive portals without compromising your DNS security setup.

Configuring LDAC Quality in PipeWire

You can set the LDAC quality between High, Standard, and Mobile quality, corresponding to 990/660/330 kbps. You can do it either statically or dynamically.

Static Configuration

Place the following configuration in ~/.config/wireplumber/wireplumber.conf.d/10-bluez.conf:

monitor.bluez.rules = [
  {
    matches = [
      {
        ## This matches all Bluetooth devices.
        device.name = "~bluez_card.*"
      }
    ]
    actions = {
      update-props = {
        bluez5.a2dp.ldac.quality = "sq"
      }
    }
  }
]

The value of quality can be set to either hq, sq, mq, or auto.

You can also change the match fragment to match only a specific device. Use the following command to list all currently available devices:

$ pw-cli ls | grep device.name

After changing the configuration, you’ll have to restart WirePlumber:

$ systemctl --user restart wireplumber

Dynamic Configuration

This method is less user-friendly. The first step is to find the id of the relevant output node. You can do this by examining the output of pw-cli ls or wpctl status. Make sure you pick the id of the corresponding node and not the device. Next, use pw-cli set-param to set the quality, for example:

$ pw-cli set-param 93 Props '{quality=0}'
Object: size 32, type Spa:Pod:Object:Param:Props (262146), id Spa:Enum:ParamId:Props (2)
  Prop: key Spa:Pod:Object:Param:Props:quality (269), flags 00000000
    Int 0

Where 93 is our node id, and 0 corresponds to hq quality. Other possible values are: -1 for auto, 0 for hq, 1 for sq, and 2 for mq.

Empirically Verifying Bitrate

You can deduce the actual bitrate by sniffing the Bluetooth traffic and analyzing the capture using Wireshark.

$ sudo btmon -w btsnoop.log
$ wireshark btsnoop.log

In Wireshark, go to Statistics -> Capture File Properties, and there you can see the average bits/s and compare it to the bitrate of the expected quality setting.

Tiered vs. Fixed Commissions in IBKR

This post compares the tiered and fixed commission models offered by Interactive Brokers for trading U.S.-nominated ETFs on the London Stock Exchange (LSE). Fixed commissions are set at 0.05% of the trade value, with a minimum charge of $4. In contrast, tiered commissions begin with a lower minimum of $1.70, but they incur additional exchange fees of 0.0045% of the trade value (with a minimum of £0.10) and a clearing fee of £0.06. While tiered pricing can be advantageous for very large trades, this aspect is less relevant for my trading needs.

In practice, for small trades up to approximately $7,200, tiered pricing tends to be slightly more beneficial, whereas fixed pricing becomes cheaper for trades exceeding that amount. However, the differences in costs remain relatively minor, as illustrated in the figures below.

Setting Up a Swap File on BTRFS

With the release of btrfs-progs 6.1 (available in Debian Bookworm), you can create a swap file using the btrfs utility. It will take care of both preallocating the file and marking it as NODATACOW. As BTRFS can’t snapshot a subvolume that contains an active swap file, we will create a new subvolume for the swap file to reside in.

$ sudo btrfs subvolume create /swap
$ sudo btrfs filesystem mkswapfile --size 4g /swap/swapfile
$ sudo swapon /swap/swapfile

To auto-activate the swap file at boot, add the following line to /etc/fstab:

/swap/swapfile none swap defaults 0 0

Encrypted swap file

My entire root filesystem is encrypted, but having unencrypted swap can still lead to sensitive data being inadvertently exposed. The solution is to encrypt the swap file using random keys at boot. Add the following line to /etc/crypttab:

swap /swap/swapfile  /dev/urandom swap,cipher=aes-xts-plain64

And change the swap file location in /etc/fstab to point to /dev/mapper/swap like this:

/dev/mapper/swap none swap defaults 0 0

The new swap file will automatically start upon boot. To start it immediately, run:

$ sudo systemctl daemon-reload
$ sudo systemctl start systemd-cryptsetup@swap.service
$ sudo swapon /dev/mapper/swap

PSI-Notify

Adding a small amount of swap can significantly help in preventing the system from running out of memory. psi-notify can alert you when your running low on memory.

$ sudo apt install psi-notify
$ systemctl --user start psi-notify

Add the following line to ~/.config/psi-notify:

threshold memory some avg10 2.00

You may need to adjust the threshold to ensure it triggers at the right time for your needs. To test memory pressure, you can use the following command:

$ </dev/zero head -c 20G | tail

Fixing the Out of Memory Error When Installing Interactive Brokers TWS on Linux

When installing Interactive Brokers TWS on Linux, I encountered the following error after the installer unpacked the Java Runtime Environment (JRE):

library initialization failed - unable to allocate file descriptor table - out of memoryAborted

The solution was to increase the open file limit before running the installer:

$ ulimit -n 10000
$ ./tws-stable-linux-x64.sh

Convert PKCS#7 Certificate Chain to PEM

I’m trying to use certificates issued by Microsoft Active Directory Certificate Services (AD CS) to connect to an 802.1x protected network. NetworkManager expects certificates in PEM format, but AD CS issues them in PKCS#7 format (with a .p7b extension). You can use OpenSSL to convert the certificates:

openssl pkcs7 -print_certs -inform DER -in certnew.p7b -out cert-chain.pem

In this command, certnew.p7b is the PKCS#7 encoded certificate chain you received from AD CS, and cert-chain.pem is the desired output file.

Running Concept2 Utility in a VM

II recently found myself needing to transfer some old workout data from my Concept2 PM3 and its associated Logcard. However, as I didn’t have easy access to a Windows machine, and Concept2 only provides their utility software for Windows and Mac, I was compelled to run it in a Windows VM.

Upon connecting the PM3 to my computer and redirecting the USB device to the VM, I encountered an issue: the Concept2 Utility failed to recognize the connected PM3. In an attempt to resolve this, I downgraded the Concept2 Utility to an older version, 6.54. This version did recognize the PM3, but it still failed to recognize the Logcard.

The solution I found was to add the PM3 as a USB host device, rather than using USB redirection. This can be accomplished via the VM’s hardware detail page by selecting Add Hardware -> USB Host Device, or by using the following XML configuration:

<hostdev mode="subsystem" type="usb" managed="yes">
  <source>
    <vendor id="0x17a4"/>
    <product id="0x0001"/>
  </source>
  <address type="usb" bus="0" port="3"/>
</hostdev>

The Vendor ID/Product ID (VID/PID) shown above corresponds to the PM3’s VID/PID. If you’re using a different monitor, you may need to adjust these values accordingly.

For reference, here is an example of how the PM3’s VID/PID appears:

Bus 003 Device 004: ID 17a4:0001 Concept2 Performance Monitor 3

Additional details about the setup include:

  • Windows 10 VM
  • QEMU/KVM virtualization via virt-manager.
  • Concept2 Utility verison 7.14

Regenerate Dracut initramfs images from a live USB

Thanks to an incompatability between dracut and systemd version 256, I was left with an unbootable system. I booted a live USB system, downgraded systemd back to 255, but I had to regenerate the initramfs images to be compatible with my old systemd.

While using systemd-nspawn is convenient for modifying the system from a live usb, generating dracut initramfs images through it, resulted in missing hardware support (for example, no nvme module) due to the abstracted hardware in the container. The solution is to revert to the traditional chroot and generate the images from there:

$ sudo /usr/lib/systemd/systemd-cryptsetup attach root /dev/nvme0n1p3
$ sudo mount /dev/mapper/root /mnt/
$ sudo mount /dev/nvme0n1p2 /mnt/boot/
$ sudo mount --bind /dev/ /mnt/dev/
$ sudo mount --bind /proc/ /mnt/proc/
$ sudo mount --bind /sys/ /mnt/sys/
$ sudo chroot /mnt/
# dracut -f --regenerate-all

Automating DNS Configurations for F5 VPN Tunnel using Systemd-resolved and NetworkManager-dispatcher

F5 VPN does not play well with split DNS configuration using systemd-resolved because it insists on trying to rewrite /etc/resolv.conf. The workaround is to make resolv.conf immutable, and configure the DNS settings for the tunnel manually. systemd-resolved does not have a mechanism for persistant per-interface configuration, and it relies on NetworkManager to configure each connection correctly. F5 VPN is not compatible with NetworkManager, and does not make it easy to configure it this way.

NetworkManager-dispatcher allows you to run scripts based on network events. In our case, we will use it to automatically add DNS configurations when the F5 VPN tunnel tun0 is up, and thus provide persistent configuration.

Here is the script:

#!/bin/bash

INTERFACE=$1
STATUS=$2

case "$STATUS" in
    'up')
        if [ "$INTERFACE" = "tun0" ]; then
            # Add your search domains here
            SEARCH_DOMAINS="~example.corp ~example.local"

            resolvectl domain "$INTERFACE" $SEARCH_DOMAINS
            resolvectl dns $INTERFACE 192.168.100.20 192.168.100.22
            resolvectl dnsovertls tun0 no
        fi
        ;;
esac

The script checks if the interface is tun0 and if the current action is up. If so, it uses resolvectl to configure search domains and local DNS servers. Lastly, DNS over TLS is disabled, as the corporate DNS servers do not support them.

To make this script work, install in the /etc/NetworkManager/dispatcher.d/ directory with the name f5-vpn. Make sure it’s executable and only writable by root. NetworkManager-dispatcher will run this script whenever a network interface goes up, automatically setting the DNS configurations for F5 VPN tunnel.