Enrico's blog

Financial risks in 2026

I asked the banker who is my reference at the bank something like this:

Give that we are talking about the consequences of the tantrum of a fascist foreign government, what happened to them (who are also people close and dear to me), in some future can very well happen to me.

Suddenly my risk profile shot up under the roof.

What do you suggest me to do? Should I find a trusted source of gold bullions to bury under the cellar at home?

The answer was something like this:

Sadly YES, given that the USA have a sort of financial monopoly they can entitle themselves to arbitrarily define a person/organization as a terrorist without any trial or judicial course, and as a consequence apply sanctions that cannot be effectively counteracted, not even abroad.

I didn't have this in my 2026 bingo card, but here we are.


For more details, see:

For some broader context on this kind of actions from the USA, see also:

Migrating away from .org/.net/.com domains

After having witnessed how easy it is for good people to lose a .org domain over a fascist tantrum (you can follow the Autistici/Inventati story here and here), I've started moving all my infrastructure to differently managed TLDs.

enricozini.org and enricozini.com will keep being functional for the time being, as dropping a domain makes it available for squatting and impersonation.

These new domains are now online, with working web and emails:

It will take ages to migrate countless accounts that are tied to my primary email address, so better start early.

Waiting to see what will happen with .meow domains, which I supported despite not identifying as a cat.

Gnome refusing to suspend

I'm tired, I want to do go bed. I click "sleep" on gnome shell, nothing happens.

Swearwords.

I want to go to bed. I might have want to put my laptop in a bag and run to catch a train. I hate when this happens.

systemd-inhibit --list --mode=block doesn't help much:

$ systemd-inhibit --list --mode=block
WHO    UID  USER   PID  COMM            WHAT                                                     WHY                        MODE
enrico 1000 enrico 3042 gsd-power       handle-lid-switch                                        External monitor attached… block
enrico 1000 enrico 3037 gsd-media-keys  handle-power-key:handle-suspend-key:handle-hibernate-key GNOME handling keypresses  block
enrico 1000 enrico 2878 gnome-session-b sleep                                                    user session inhibited     block

After much googling I found out about gnome-session-inhibit:

$ gnome-session-inhibit  --list
mutter: idle-inhibit (idle)
/usr/lib/chromium/chromium: Playing audio (suspend)

Found the right tab in chromium, paused playing, sleep works again.

My sleep was a good half an hour overdue, and all I got for it was to write this blog post.

Of course Gnome could have shown me its inhibitor list instead of doing nothing, since it has that information, but it didn't.

What I really would expect is that if I intentionally click a suspend button, audio and video playing wouldn't inhibit the suspend. Maybe in a future version of Gnome?

Systemd Nspawn and Fedora 44

Fedora 44 is out and I need to add support for it Moncic-CI.

What could possibly go wrong?

rpmbuild cannot build packages

Well, when using systemd-nspawn as a backend, rpmbuild cannot build packages:

+ rpmbuild -ba --define 'srcarchivename hello' /srv/moncic-ci/source/hello/fedora/SPECS/hello.spec
Building target platforms: noarch
Building for target noarch
setting SOURCE_DATE_EPOCH=1748563200
Executing(%mkbuilddir): /bin/sh -e /var/tmp/rpm-tmp.NEQnsa
Executing(%prep): /bin/sh -e /var/tmp/rpm-tmp.aE2raB
+ umask 022
+ cd /root/rpmbuild/BUILD/hello-1.0-build
+ cd /root/rpmbuild/BUILD/hello-1.0-build
+ rm -rf hello
+ /usr/lib/rpm/rpmuncompress -x /root/rpmbuild/SOURCES/hello.tar.gz
/usr/bin/tar: hello/debian: Cannot mkdir: Function not implemented
/usr/bin/tar: hello/debian/changelog: Cannot open: Function not implemented
/usr/bin/tar: hello/debian/compat: Cannot open: Function not implemented
/usr/bin/tar: hello/debian/control: Cannot open: Function not implemented
/usr/bin/tar: hello/debian/copyright: Cannot open: Function not implemented
/usr/bin/tar: hello/debian/hello.install: Cannot open: Function not implemented
/usr/bin/tar: hello/debian/rules: Cannot open: Function not implemented
/usr/bin/tar: hello/debian/source: Cannot mkdir: Function not implemented
/usr/bin/tar: hello/debian/source/format: Cannot open: Function not implemented
/usr/bin/tar: hello/fedora: Cannot mkdir: Function not implemented
/usr/bin/tar: hello/fedora/SPECS: Cannot mkdir: Function not implemented
/usr/bin/tar: hello/fedora/SPECS/hello.spec: Cannot open: Function not implemented
/usr/bin/tar: hello/hello: Cannot open: Function not implemented
/usr/bin/tar: Exiting with failure status due to previous errors
error: Bad exit status from /var/tmp/rpm-tmp.aE2raB (%prep)
    Bad exit status from /var/tmp/rpm-tmp.aE2raB (%prep)

Ok, what's wrong with rpmuncompress?

$ /usr/lib/rpm/rpmuncompress -x hello.tar.gz  -v
 /usr/bin/gzip -dc  'hello.tar.gz' | /usr/bin/tar -xvvof -

Tar cannot untar...

Is that all that it's really doing? Is tar not working now?

$ tar -axf hello.tar.gz
tar: hello/debian: Cannot mkdir: Function not implemented
tar: hello/debian/changelog: Cannot open: Function not implemented
tar: hello/debian/copyright: Cannot open: Function not implemented
tar: hello/debian/hello.install: Cannot open: Function not implemented
tar: hello/debian/rules: Cannot open: Function not implemented
tar: hello/debian/source: Cannot mkdir: Function not implemented
tar: hello/debian/source/format: Cannot open: Function not implemented
tar: hello/debian/compat: Cannot open: Function not implemented
tar: hello/debian/control: Cannot open: Function not implemented
tar: hello/fedora: Cannot mkdir: Function not implemented
tar: hello/fedora/SPECS: Cannot mkdir: Function not implemented
tar: hello/fedora/SPECS/hello.spec: Cannot open: Function not implemented
tar: hello/hello: Cannot open: Function not implemented
tar: Exiting with failure status due to previous errors

Yes. Tar indeed appears not to be working.

$ strace tar -axf hello.tar.gz
…
openat2(AT_FDCWD, "hello/", …, 24) = -1 ENOSYS (Function not implemented)

Oooh, new tar is using openat2, that's really cool! I'm a big fan of that set of syscalls, and, I agree with what Lennart wrote just the other day, that focussing solely on memory stuff ignoring the fs stuff is just bad security engineering. Cheers to tar!

However, there seems to be a seccomp configuration in place that disables openat2.

I looked around the internet for a bit and this systemd pull request seems relevant, although I haven't yet traced all the steps of what's going on to be able to confirm or deny it.

...except when tar can untar

Compare this:

# tar -C /  -acf - root | tar -C /tmp -avxf -
root/
root/.ssh/
tar: root/.ssh: Cannot mkdir: Function not implemented
root/.bash_logout
tar: root/.bash_logout: Cannot open: Function not implemented
root/.bash_profile
tar: root/.bash_profile: Cannot open: Function not implemented
root/.bashrc
tar: root/.bashrc: Cannot open: Function not implemented
root/.cshrc
tar: root/.cshrc: Cannot open: Function not implemented
root/.tcshrc
tar: root/.tcshrc: Cannot open: Function not implemented
root/.bash_history
tar: root/.bash_history: Cannot open: Function not implemented
tar: Exiting with failure status due to previous errors

With this:

# tar -C /root  -acf - . | tar -C /tmp -avxf -
./
./.ssh/
./.bash_logout
./.bash_profile
./.bashrc
./.cshrc
./.tcshrc
./.bash_history

Now what? Let's strace again.

In the first case, it uses openat2:

# mkdir /tmp/bad && tar -C /  -acf - root | strace tar -C /tmp/bad -axf -
…
openat(AT_FDCWD, "/tmp/bad", O_RDONLY|O_CLOEXEC|O_PATH|O_DIRECTORY) = 3
mkdirat(3, "root", 0500)                = 0
openat2(3, "root/", {flags=O_RDONLY|O_NOFOLLOW|O_CLOEXEC|O_PATH|O_DIRECTORY, resolve=RESOLVE_BENEATH}, 24) = -1 ENOSYS (Function not implemented)
write(2, "tar: ", 5tar: )                    = 5
write(2, "root/.ssh: Cannot mkdir", 23root/.ssh: Cannot mkdir) = 23
write(2, ": Function not implemented", 26: Function not implemented) = 26
write(2, "\n", 1
)                       = 1
openat2(3, "root/", {flags=O_RDONLY|O_NOFOLLOW|O_CLOEXEC|O_PATH|O_DIRECTORY, resolve=RESOLVE_BENEATH}, 24) = -1 ENOSYS (Function not implemented)
write(2, "tar: ", 5tar: )                    = 5
write(2, "root/.bash_logout: Cannot open", 30root/.bash_logout: Cannot open) = 30
write(2, ": Function not implemented", 26: Function not implemented) = 26
write(2, "\n", 1
)                       = 1
openat2(3, "root/", {flags=O_RDONLY|O_NOFOLLOW|O_CLOEXEC|O_PATH|O_DIRECTORY, resolve=RESOLVE_BENEATH}, 24) = -1 ENOSYS (Function not implemented)
write(2, "tar: ", 5tar: )                    = 5
write(2, "root/.bash_profile: Cannot open", 31root/.bash_profile: Cannot open) = 31
write(2, ": Function not implemented", 26: Function not implemented) = 26
write(2, "\n", 1
)                       = 1
openat2(3, "root/", {flags=O_RDONLY|O_NOFOLLOW|O_CLOEXEC|O_PATH|O_DIRECTORY, resolve=RESOLVE_BENEATH}, 24) = -1 ENOSYS (Function not implemented)
write(2, "tar: ", 5tar: )                    = 5
write(2, "root/.bashrc: Cannot open", 25root/.bashrc: Cannot open) = 25
write(2, ": Function not implemented", 26: Function not implemented) = 26
write(2, "\n", 1
)                       = 1
openat2(3, "root/", {flags=O_RDONLY|O_NOFOLLOW|O_CLOEXEC|O_PATH|O_DIRECTORY, resolve=RESOLVE_BENEATH}, 24) = -1 ENOSYS (Function not implemented)
write(2, "tar: ", 5tar: )                    = 5
write(2, "root/.cshrc: Cannot open", 24root/.cshrc: Cannot open) = 24
write(2, ": Function not implemented", 26: Function not implemented) = 26
write(2, "\n", 1
)                       = 1
openat2(3, "root/", {flags=O_RDONLY|O_NOFOLLOW|O_CLOEXEC|O_PATH|O_DIRECTORY, resolve=RESOLVE_BENEATH}, 24) = -1 ENOSYS (Function not implemented)
write(2, "tar: ", 5tar: )                    = 5
write(2, "root/.tcshrc: Cannot open", 25root/.tcshrc: Cannot open) = 25
write(2, ": Function not implemented", 26: Function not implemented) = 26
write(2, "\n", 1
)                       = 1
openat2(3, "root/", {flags=O_RDONLY|O_NOFOLLOW|O_CLOEXEC|O_PATH|O_DIRECTORY, resolve=RESOLVE_BENEATH}, 24) = -1 ENOSYS (Function not implemented)
write(2, "tar: ", 5tar: )                    = 5
write(2, "root/.bash_history: Cannot open", 31root/.bash_history: Cannot open) = 31
write(2, ": Function not implemented", 26: Function not implemented) = 26
write(2, "\n", 1
)                       = 1
close(0)                                = 0
utimensat(3, "root", [UTIME_OMIT, {tv_sec=1777462709, tv_nsec=0} /* 2026-04-29T13:38:29+0200 */], AT_SYMLINK_NOFOLLOW) = 0
fchownat(3, "root", 0, 0, AT_SYMLINK_NOFOLLOW) = 0
fchmodat2(3, "root", 0550, AT_SYMLINK_NOFOLLOW) = 0
write(2, "tar: ", 5tar: )                    = 5
write(2, "Exiting with failure status due "..., 50Exiting with failure status due to previous errors) = 50
write(2, "\n", 1

In the second case, it does not:

# mkdir /tmp/good && tar -C /root  -acf - . | strace tar -C /tmp/good -axf -
…
openat(AT_FDCWD, "/tmp/good", O_RDONLY|O_CLOEXEC|O_PATH|O_DIRECTORY) = 3
mkdirat(3, ".", 0500)                   = -1 EEXIST (File exists)
newfstatat(3, ".", {st_mode=S_IFDIR|0755, st_size=40, ...}, AT_SYMLINK_NOFOLLOW) = 0
newfstatat(3, ".", {st_mode=S_IFDIR|0755, st_size=40, ...}, AT_SYMLINK_NOFOLLOW) = 0
mkdirat(3, ".ssh", 0700)                = 0
utimensat(3, ".ssh", [UTIME_OMIT, {tv_sec=1777462478, tv_nsec=0} /* 2026-04-29T13:34:38+0200 */], AT_SYMLINK_NOFOLLOW) = 0
fchownat(3, ".ssh", 0, 0, AT_SYMLINK_NOFOLLOW) = 0
openat(3, ".bash_logout", O_WRONLY|O_CREAT|O_EXCL|O_NOCTTY|O_NONBLOCK|O_CLOEXEC, 0600) = 4
write(4, "# ~/.bash_logout\n\n", 18)    = 18
utimensat(4, NULL, [UTIME_OMIT, {tv_sec=1768608000, tv_nsec=0} /* 2026-01-17T01:00:00+0100 */], 0) = 0
fchown(4, 0, 0)                         = 0
fchmod(4, 0644)                         = 0
close(4)                                = 0
openat(3, ".bash_profile", O_WRONLY|O_CREAT|O_EXCL|O_NOCTTY|O_NONBLOCK|O_CLOEXEC, 0600) = 4
write(4, "# .bash_profile\n\n# Get the alias"..., 144) = 144
utimensat(4, NULL, [UTIME_OMIT, {tv_sec=1768608000, tv_nsec=0} /* 2026-01-17T01:00:00+0100 */], 0) = 0
fchown(4, 0, 0)                         = 0
fchmod(4, 0644)                         = 0
close(4)                                = 0
openat(3, ".bashrc", O_WRONLY|O_CREAT|O_EXCL|O_NOCTTY|O_NONBLOCK|O_CLOEXEC, 0600) = 4
write(4, "# .bashrc\n\n# Source global defin"..., 575) = 575
utimensat(4, NULL, [UTIME_OMIT, {tv_sec=1768608000, tv_nsec=0} /* 2026-01-17T01:00:00+0100 */], 0) = 0
fchown(4, 0, 0)                         = 0
fchmod(4, 0644)                         = 0
close(4)                                = 0
openat(3, ".cshrc", O_WRONLY|O_CREAT|O_EXCL|O_NOCTTY|O_NONBLOCK|O_CLOEXEC, 0600) = 4
write(4, "# .cshrc\n\n# User specific aliase"..., 100) = 100
utimensat(4, NULL, [UTIME_OMIT, {tv_sec=1768608000, tv_nsec=0} /* 2026-01-17T01:00:00+0100 */], 0) = 0
fchown(4, 0, 0)                         = 0
fchmod(4, 0644)                         = 0
close(4)                                = 0
openat(3, ".tcshrc", O_WRONLY|O_CREAT|O_EXCL|O_NOCTTY|O_NONBLOCK|O_CLOEXEC, 0600) = 4
write(4, "# .tcshrc\n\n# User specific alias"..., 129) = 129
utimensat(4, NULL, [UTIME_OMIT, {tv_sec=1768608000, tv_nsec=0} /* 2026-01-17T01:00:00+0100 */], 0) = 0
fchown(4, 0, 0)                         = 0
fchmod(4, 0644)                         = 0
close(4)                                = 0
openat(3, ".bash_history", O_WRONLY|O_CREAT|O_EXCL|O_NOCTTY|O_NONBLOCK|O_CLOEXEC, 0600) = 4
write(4, "dnf install rpmutils\ndnf install"..., 105) = 105
utimensat(4, NULL, [UTIME_OMIT, {tv_sec=1777462709, tv_nsec=0} /* 2026-04-29T13:38:29+0200 */], 0) = 0
fchown(4, 0, 0)                         = 0
close(4)                                = 0
close(0)                                = 0
newfstatat(3, ".", {st_mode=S_IFDIR|0755, st_size=180, ...}, 0) = 0
utimensat(3, ".", [UTIME_OMIT, {tv_sec=1777462709, tv_nsec=0} /* 2026-04-29T13:38:29+0200 */], 0) = 0
fchownat(3, ".", 0, 0, 0)               = 0
fchmodat(3, ".", 0550)                  = 0

Narrowing down the problem

systemd-nspawn -D works

$ sudo systemd-nspawn --ephemeral -D /var/lib/machines/fedora44
[root@fedora44-da319fe9130f3407 ~]# tar -C / -cf - root | tar -C /tmp -xf -
[root@fedora44-da319fe9130f3407 ~]# tar -C /root -cf - . | tar -C /tmp -xf -

machinectl works

$ sudo machinectl start fedora44
$ sudo machinectl shell fedora44
Connected to machine fedora44. Press ^] three times within 1s to exit session.
[root@fedora44 ~]# tar -C / -cf - root | tar -C /tmp -xf -
[root@fedora44 ~]# tar -C /root -cf - . | tar -C /tmp -xf -
[root@fedora44 ~]# cat /etc/os-release
NAME="Fedora Linux"
VERSION="44 (Forty Four)"
…

machinectl + systemd-run works

$ sudo machinectl start fedora44
$ sudo systemd-run --machine fedora44 --wait --tty sh -c 'tar -C / -cf - root | tar -C /tmp -xf -'
Running as unit: run-p431602-i431902.service
Press ^] three times within 1s to disconnect TTY.

Finished with result: success
Main processes terminated with: code=exited, status=0/SUCCESS
Service runtime: 33ms
CPU time consumed: 10ms
Memory peak: 1.3M (swap: 0B)
$ sudo systemd-run --machine fedora44 --wait --tty sh -c 'tar -C /root -cf - . | tar -C /tmp -xf -'
Running as unit: run-p431638-i431938.service
Press ^] three times within 1s to disconnect TTY.

Finished with result: success
Main processes terminated with: code=exited, status=0/SUCCESS
Service runtime: 29ms
CPU time consumed: 10ms
Memory peak: 1.3M (swap: 0B)

systemd-nspawn + systemd-run works

$ sudo systemd-nspawn --ephemeral -D /var/lib/machines/fedora44 --machine test --boot
$ sudo systemd-run --machine test --wait --tty sh -c 'tar -C / -cf - root | tar -C /tmp -xf -'
[sudo] password for enrico:
Running as unit: run-p431915-i432215.service
Press ^] three times within 1s to disconnect TTY.

Finished with result: success
Main processes terminated with: code=exited, status=0/SUCCESS
Service runtime: 62ms
CPU time consumed: 10ms
Memory peak: 2.1M (swap: 0B)
$ sudo systemd-run --machine test --wait --tty sh -c 'tar -C /root -cf - . | tar -C /tmp -xf -'
Running as unit: run-p431995-i432295.service; invocation ID: 99c164ebae684bb99df71f9230ecef7c
Press ^] three times within 1s to disconnect TTY.

Finished with result: success
Main processes terminated with: code=exited, status=0/SUCCESS
Service runtime: 49ms
CPU time consumed: 11ms
Memory peak: 1.8M (swap: 0B)

systemd-nspawn --suppress-sync=true + systemd-run does not work

So, what is Moncic-CI doing that I'm not?

It's a CI running tasks in throwaway ephemeral machines, so it uses --suppress-sync=true for performance. Let's see...

$ sudo systemd-nspawn --ephemeral --suppress-sync=true -D /var/lib/machines/fedora44 --machine test --boot
$ sudo systemd-run --machine test --wait --tty sh -c 'tar -C / -cf - root | tar -C /tmp -xf -'
Running as unit: run-p432437-i432737.service
Press ^] three times within 1s to disconnect TTY.
tar: root/.ssh: Cannot mkdir: Function not implemented
tar: root/.bash_logout: Cannot open: Function not implemented
tar: root/.bash_profile: Cannot open: Function not implemented
tar: root/.bashrc: Cannot open: Function not implemented
tar: root/.cshrc: Cannot open: Function not implemented
tar: root/.tcshrc: Cannot open: Function not implemented
tar: root/.bash_history: Cannot open: Function not implemented
tar: Exiting with failure status due to previous errors
Finished with result: exit-code
Main processes terminated with: code=exited, status=2/INVALIDARGUMENT
Service runtime: 62ms
CPU time consumed: 13ms
Memory peak: 1.8M (swap: 0B)

Getting warmer there...

systemd-nspawn --suppress-sync=true does not work

$ sudo systemd-nspawn --ephemeral --suppress-sync=true -D /var/lib/machines/fedora44
  Spawning container fedora44-233d2e0e6853964c on /var/lib/machines/.#machine.fedora44655d618f26cb4d20.
  Press Ctrl-] three times within 1s to kill container.
[root@fedora44-233d2e0e6853964c ~]# tar -C / -cf - root | tar -C /tmp -xf -
tar: root/.ssh: Cannot mkdir: Function not implemented
tar: root/.bash_logout: Cannot open: Function not implemented
tar: root/.bash_profile: Cannot open: Function not implemented
tar: root/.bashrc: Cannot open: Function not implemented
tar: root/.cshrc: Cannot open: Function not implemented
tar: root/.tcshrc: Cannot open: Function not implemented
tar: root/.bash_history: Cannot open: Function not implemented
tar: Exiting with failure status due to previous errors
[root@fedora44-233d2e0e6853964c ~]#

Looks like a systemd-nspawn bug...

...filed as https://github.com/systemd/systemd/issues/41868

Edited to add: Marcin Juszkiewicz pointed out this is already known as RedHat bug #2437037.

...with a simple workaround

...and pushed a commit that disables (for now) --suppress-sync when building on Fedora 44 containers.

With the workaround integration tests are now finally passing!

CAdES signatures on Debian

CAdES is a digital signature standard that is used and sometimes mandated, by the Italian Public Administration.

To be able to do my job, I own a Carta Nazionale dei Servizi (CNS) with which I can generate legally binding signatures. Now comes the problem of finding a software to do it.

Infocamere Firma4NG

InfoCamere are distributing a software called Firma4NG, with a Linux option, which, I'm pleased to say, seems to work just fine.

Autofirma

AutoFirma is a Java software for digital signatures distributed by the Spanish government, which has a Linux version.

It is licensed as GPL-2+ | EUPL-1.1, and the source seems to be here.

While my Spanish is decent I lack jargon for this specific field, and I didn't manage to make it work with my CNS.

Autogram

Andrej Shadura pointed me to Autogram, a Slovakian software for digital signatures, licensed under the EUPL-1.2.

The interface is still only in Slovakian, so tried it but I didn't go very far in trying to make it work.

OpenSSL

In trixie, openssl is almost, but not quite, able to do it. Here's as far as I've got.

Install opensc

apt install opensc

Test if you can access the smart card with:

pkcs11-tool --list-objects [-l]

You can find other pkcs11-tool examples here

Set up a pkcs11 provider for openssl

apt install pkcs11-provider

Edit /etc/ssl/openssl.cnf:

  • In [provider_sect] add pkcs11 = pkcs11_sect
  • In [default_sect], uncomment activate = 1
  • Add this new section:
[pkcs11_sect]
module = /usr/lib/x86_64-linux-gnu/ossl-modules/pkcs11.so
pkcs11-module-path = /usr/lib/x86_64-linux-gnu/pkcs11/opensc-pkcs11.so
default_algorithms = ALL
activate = 1

Test with openssl list -providers

You can check if openssl can see keys on the card:

openssl pkey -in 'pkcs11:id=%01' -pubin -pubout -text

See PKCS11 URI documentation here.

Install the PKCS11 engine for openssl

apt install libengine-pkcs11-openssl

It looks like providers replaced engines, and this would not be needed, but I couldn't find a way to convince openssl to work without this.

Sign a document

openssl cms -nodetach -binary -cades -outform DER -in filename -out filename.p7m -sign -signer 'pkcs11:id=%01' -keyform engine -engine pkcs11

It verifies correctly using the Austrian verification system.

All the Italian verification systems I tried, however, complain that, although the signature is valid, the certificate is emitted by an unqualified CA and the certificate revocation information cannot be found.

PAdES

When signing PDF files, the PAdES standard is sometimes accepted.

LibreOffice is able to generate PAdES signatures using the "File / Digital signatures…" menu, and provided the smart card is in the reader it is able to use it. Both LibreOffice and Okular can verify that the signature is indeed there.

However, when trying to validate the signature using Italian validators, I get the same complaints about unqualified CAs and missing revocation information.

Wall of shame

Dike GoSign

Infocert (now Tinexta) used to distribute a software called "Dike GoSign" that worked on Ubuntu, which I used on a completely isolated VM, and it was awful but it worked.

I had to regenerate the VM for it, and discovered that the version they distribute now will refuse to work unless one signs in online with a Tinexta account. From the same company that asks you to install their own root certifiactes to use their digital signature system.

Gross.

Dropped.

Aruba Sign

Aruba used to distribute a software called Aruba Sign, which also worked on Ubuntu.

Ubuntu support has been discontinued, and they now only offer support for Windows or Mac.

Yuck. Dropped.

Python-like abspath for c++

Python's os.path.abspath or Path.absolute are great: you give them a path, which might not exist, and you get a path you can use regardless of the current directory. os.path.abspath will also normalize it, while Path will not by default because with Paths a normal form is less needed.

This is great to normalize input, regardless of if it's an existing file you're needing to open, or a new file you're needing to create.

In C++17, there is a filesystem library with methods with enticingly similar names, but which are almost, but not quite, totally unlike Python's abspath.

Because in my C++ code I need to normalize input, regardless of if it's an existing file I'm needing to open or a new file I'm needing to create, here's an apparently working Python-like abspath for C++ implemented on top of the std::filesystem library:

std::filesystem::path abspath(const std::filesystem::path& path)
{
    // weakly_canonical is defined as "the result of calling canonical() with a
    // path argument composed of the leading elements of p that exist (as
    // determined by status(p) or status(p, ec)), if any, followed by the
    // elements of p that do not exist."
    //
    // This means that if no lead components of the path exist then the
    // resulting path is not made absolute, and we need to work around that.
    if (!path.is_absolute())
        return abspath(std::filesystem::current_path() / path);

    // This is further and needlessly complicated because we need to work
    // around https://gcc.gnu.org/bugzilla/show_bug.cgi?id=118733
    unsigned retry = 0;
    while (true)
    {
        std::error_code code;
        auto result = std::filesystem::weakly_canonical(path, code);
        if (!code)
        {
            // fprintf(stderr, "%s: ok in %u tries\n", path.c_str(), retry+1);
            return result;
        }

        if (code == std::errc::no_such_file_or_directory)
        {
            ++retry;
            if (retry > 50)
                throw std::system_error(code);
        }
        else
            throw std::system_error(code);
    }

    // Alternative implementation that however may not work on all platforms
    // since, formally, "[std::filesystem::absolute] Implementations are
    // encouraged to not consider p not existing to be an error", but they do
    // not mandate it, and if they did, they might still be affected by the
    // undefined behaviour outlined in https://gcc.gnu.org/bugzilla/show_bug.cgi?id=118733
    //
    // return std::filesystem::absolute(path).lexically_normal();
}

I added it to my wobble code repository, which is the thin repository of components I use to ease my C++ systems programming.

Debugging printing to a remote printer

I upgraded to Debian testing/trixie, and my network printer stopped appearing in print dialogs. These are notes from the debugging session.

Check firewall configuration

I tried out kde, which installed plasma-firewall, which installed firewalld, which closed by default the ports used for printing.

For extra fun, appindicators are not working in Gnome and so firewall-applet is currently useless, although one can run firewall-config manually, or use the command line that might be more user friendly than the UI.

Step 1: change the zone for the home wifi to "Home":

firewall-cmd  --zone home --list-interfaces
firewall-cmd  --zone home --add-interface wlp1s0

Step 2: make sure the home zone can print:

firewall-cmd --zone home --list-services
firewall-cmd --zone home --add-service=ipp
firewall-cmd --zone home --add-service=ipp-client
firewall-cmd --zone home --add-service=mdns

I searched and searched but I could not find out whether ipp is needed, ipp-client is needed, or both are needed.

Check if avahi can see the printer

Is the printer advertised correctly over mdns?

When it didn't work:

$ avahi-browse -avrt
= wlp1s0 IPv6 Brother HL-2030 series @ server                UNIX Printer         local
   hostname = [server.local]
   address = [...ipv6 address...]
   port = [0]
   txt = []
= wlp1s0 IPv4 Brother HL-2030 series @ server                UNIX Printer         local
   hostname = [server.local]
   address = [...ipv4 address...]
   port = [0]
   txt = []

$ avahi-browse -rt _ipp._tcp
[empty]

When it works:

$ avahi-browse -avrt
= wlp1s0 IPv6 Brother HL-2030 series @ server                Secure Internet Printer local
   hostname = [server.local]
   address = [...ipv6 address...]
   port = [631]
   txt = ["printer-type=0x1046" "printer-state=3" "Copies=T" "TLS=1.2" "UUID=…" "URF=DM3" "pdl=application/octet-stream,application/pdf,application/postscript,image/jpeg,image/png,image/pwg-raster,image/urf" "product=(HL-2030 series)" "priority=0" "note=" "adminurl=https://server.local.:631/printers/Brother_HL-2030_series" "ty=Brother HL-2030 series, using brlaser v6" "rp=printers/Brother_HL-2030_series" "qtotal=1" "txtvers=1"]
= wlp1s0 IPv6 Brother HL-2030 series @ server                UNIX Printer         local
   hostname = [server.local]
   address = [...ipv6 address...]
   port = [0]
   txt = []
= wlp1s0 IPv4 Brother HL-2030 series @ server                Secure Internet Printer local
   hostname = [server.local]
   address = [...ipv4 address...]
   port = [631]
   txt = ["printer-type=0x1046" "printer-state=3" "Copies=T" "TLS=1.2" "UUID=…" "URF=DM3" "pdl=application/octet-stream,application/pdf,application/postscript,image/jpeg,image/png,image/pwg-raster,image/urf" "product=(HL-2030 series)" "priority=0" "note=" "adminurl=https://server.local.:631/printers/Brother_HL-2030_series" "ty=Brother HL-2030 series, using brlaser v6" "rp=printers/Brother_HL-2030_series" "qtotal=1" "txtvers=1"]
= wlp1s0 IPv4 Brother HL-2030 series @ server                UNIX Printer         local
   hostname = [server.local]
   address = [...ipv4 address...]
   port = [0]
   txt = []

$ avahi-browse -rt _ipp._tcp
+ wlp1s0 IPv6 Brother HL-2030 series @ server                Internet Printer     local
+ wlp1s0 IPv4 Brother HL-2030 series @ server                Internet Printer     local
= wlp1s0 IPv4 Brother HL-2030 series @ server                Internet Printer     local
   hostname = [server.local]
   address = [...ipv4 address...]
   port = [631]
   txt = ["printer-type=0x1046" "printer-state=3" "Copies=T" "TLS=1.2" "UUID=…" "URF=DM3" "pdl=application/octet-stream,application/pdf,application/postscript,image/jpeg,image/png,image/pwg-raster,image/urf" "product=(HL-2030 series)" "priority=0" "note=" "adminurl=https://server.local.:631/printers/Brother_HL-2030_series" "ty=Brother HL-2030 series, using brlaser v6" "rp=printers/Brother_HL-2030_series" "qtotal=1" "txtvers=1"]
= wlp1s0 IPv6 Brother HL-2030 series @ server                Internet Printer     local
   hostname = [server.local]https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1092109
   address = [...ipv6 address...]
   port = [631]
   txt = ["printer-type=0x1046" "printer-state=3" "Copies=T" "TLS=1.2" "UUID=…" "URF=DM3" "pdl=application/octet-stream,application/pdf,application/postscript,image/jpeg,image/png,image/pwg-raster,image/urf" "product=(HL-2030 series)" "priority=0" "note=" "adminurl=https://server.local.:631/printers/Brother_HL-2030_series" "ty=Brother HL-2030 series, using brlaser v6" "rp=printers/Brother_HL-2030_series" "qtotal=1" "txtvers=1"]

Check if cups can see the printer

From CUPS' Using Network Printers:

$ /usr/sbin/lpinfo --include-schemes dnssd -v

network dnssd://Brother%20HL-2030%20series%20%40%20server._ipp._tcp.local/cups?uuid=

Debugging session interrupted

At this point, the printer appeared.

It could be that:

In the end, debugging failed successfully, and this log now remains as a reference for possible further issues.

ncdu on files to back up

I use borg and restic to backup files in my system. Sometimes I run a huge download or clone a large git repo and forget to mark it with CACHEDIR.TAG, and it gets picked up slowing the backup process and wasting backup space uselessly.

I would like to occasionally audit the system to have an idea of what is a candidate for backup. ncdu would be great for this, but it doesn't know about backup exclusion filters.

Let's teach it then.

Here's a script that simulates a backup and feeds the results to ncdu:

#!/usr/bin/python3

import argparse
import os
import sys
import time
import stat
import json
import subprocess
import tempfile
from pathlib import Path
from typing import Any

FILTER_ARGS = [
    "--one-file-system",
    "--exclude-caches",
    "--exclude",
    "*/.cache",
]
BACKUP_PATHS = [
    "/home",
]


class Dir:
    """
    Dispatch borg output into a hierarchical directory structure.

    borg prints a flat file list, ncdu needs a hierarchical JSON.
    """

    def __init__(self, path: Path, name: str):
        self.path = path
        self.name = name
        self.subdirs: dict[str, "Dir"] = {}
        self.files: list[str] = []

    def print(self, indent: str = "") -> None:
        for name, subdir in self.subdirs.items():
            print(f"{indent}{name:}/")
            subdir.print(indent + " ")
        for name in self.files:
            print(f"{indent}{name}")

    def add(self, parts: tuple[str, ...]) -> None:
        if len(parts) == 1:
            self.files.append(parts[0])
            return

        subdir = self.subdirs.get(parts[0])
        if subdir is None:
            subdir = Dir(self.path / parts[0], parts[0])
            self.subdirs[parts[0]] = subdir

        subdir.add(parts[1:])

    def to_data(self) -> list[Any]:
        res: list[Any] = []
        st = self.path.stat()
        res.append(self.collect_stat(self.name, st))
        for name, subdir in self.subdirs.items():
            res.append(subdir.to_data())

        dir_fd = os.open(self.path, os.O_DIRECTORY)
        try:
            for name in self.files:
                try:
                    st = os.lstat(name, dir_fd=dir_fd)
                except FileNotFoundError:
                    print(
                        "Possibly broken encoding:",
                        self.path,
                        repr(name),
                        file=sys.stderr,
                    )
                    continue
                if stat.S_ISDIR(st.st_mode):
                    continue
                res.append(self.collect_stat(name, st))
        finally:
            os.close(dir_fd)

        return res

    def collect_stat(self, fname: str, st) -> dict[str, Any]:
        res = {
            "name": fname,
            "ino": st.st_ino,
            "asize": st.st_size,
            "dsize": st.st_blocks * 512,
        }
        if stat.S_ISDIR(st.st_mode):
            res["dev"] = st.st_dev
        return res


class Scanner:
    def __init__(self) -> None:
        self.root = Dir(Path("/"), "/")
        self.data = None

    def scan(self) -> None:
        with tempfile.TemporaryDirectory() as tmpdir_name:
            mock_backup_dir = Path(tmpdir_name) / "backup"
            subprocess.run(
                ["borg", "init", mock_backup_dir.as_posix(), "--encryption", "none"],
                cwd=Path.home(),
                check=True,
            )

            proc = subprocess.Popen(
                [
                    "borg",
                    "create",
                    "--list",
                    "--dry-run",
                ]
                + FILTER_ARGS
                + [
                    f"{mock_backup_dir}::test",
                ]
                + BACKUP_PATHS,
                cwd=Path.home(),
                stderr=subprocess.PIPE,
            )
            assert proc.stderr is not None
            for line in proc.stderr:
                match line[0:2]:
                    case b"- ":
                        path = Path(line[2:].strip().decode())
                    case b"x ":
                        continue
                    case _:
                        raise RuntimeError(f"Unparsable borg output: {line!r}")

                if path.parts[0] != "/":
                    raise RuntimeError(f"Unsupported path: {path.parts!r}")
                self.root.add(path.parts[1:])

    def to_json(self) -> list[Any]:
        return [
            1,
            0,
            {
                "progname": "backup-ncdu",
                "progver": "0.1",
                "timestamp": int(time.time()),
            },
            self.root.to_data(),
        ]

    def export(self):
        return json.dumps(self.to_json()).encode()


def main():
    parser = argparse.ArgumentParser(
        description="Run ncdu to estimate sizes of files to backup."
    )
    parser.parse_args()

    scanner = Scanner()
    scanner.scan()
    # scanner.root.print()
    res = subprocess.run(["ncdu", "-f-"], input=scanner.export())
    sys.exit(res.returncode)


if __name__ == "__main__":
    main()

Disable spellchecker popup on Android

On Android, there's a spellchecker popup that occasionally appears over the keyboard, getting very annoyingly in the way. See for example this unanswered question with screenshots.

It looks like a feature of the keyboard, but it's not, and so I looked and I looked and I could not find how to turn it off.

The answer is to look for how to disable the spellchecker in the keyboard section of the android system settings, not in the android keyboard app settings.

See for example this answer on stackexchange.

How to right click

I climbed on top of a mountain with a beautiful view, and when I started readying my new laptop for a work call (as one does on top of mountains), I realised that I couldn't right click and it kind of spoiled the mood.

Clicking on the bottom right corner of my touchpad left-clicked. Clicking with two fingers left-clicked. Alt-clicking, Super-clicking, Control-clicking, left clicked.

Here's there are two ways to simulate mouse buttons with touchpads in Wayland:

  • clicking on different areas at the bottom of the touchpad
  • double or triple-tapping, as long as the fingers are not too far apart

Skippable digression:

I'm not sure why Gnome insists in following Macs for defaults, which is what people with non-Mac hardware are less likely to be used to.

In my experience, Macs are as arbitrarily awkward to use as anything else, but they managed to build a community where if you don't understand how it works you get told you're stupid. All other systems (including Gnome) have communities where instead you get told (as is generally the case) that the system design is stupid, which at least gives you some amount of validation in your suffering.

Oh well.

How to configure right click

Surprisingly, this is not available in Gnome Shell settings. It can be found in gnome-tweaks: under "Keyboard & Mouse", "Mouse Click Emulation", one can choose between "Fingers" or "Area".

I tried both and went for "Area": I use right-drag a lot to resize windows, and I couldn't find a way, at least with this touchpad, to make it work consistently in "Fingers" mode.

© 2026 Enrico Zini. Generated with staticsite on 2026-09-10 02:00 CEST.