Issue 1: Literal “~” Directory in /home/amnesia
On Tails 7.9, a directory literally named ~ appears inside /home/amnesia/, containing the path ~/~/.local/share/applications/ (where the first ~ is the actual home directory and the second is a literal directory name). The applications subdirectory is empty. This did not exist in Tails 7.8.1.
Root Cause
This is caused by commit df27871, which introduced a new systemd user service to hide the GNOME Software launcher unless the Flatpak feature flag is enabled. The service file is tails-possibly-hide-gnome-software-launcher.service:
[Unit]
Description=Hide GNOME Software if Flatpak is not enabled
After=gnome-session.target
[Service]
Type=oneshot
ExecStart=mkdir -p ~/.local/share/applications/
ExecStart=/bin/sh -c "(cat /usr/share/applications/org.gnome.Software.desktop \
&& echo 'NoDisplay=true') >> ~/.local/share/applications/org.gnome.Software.desktop"
The problem is on the first ExecStart line. In systemd service files, the tilde (~) is not expanded to the user’s home directory. Systemd interprets it as a literal directory name. According to the systemd.exec documentation, only %h, $HOME, or wrapping the command in /bin/sh -c achieve home directory expansion.
The second ExecStart line works correctly because it wraps the command in /bin/sh -c, where the shell does expand ~ properly. But the first mkdir -p ~/.local/share/applications/ runs directly (no shell wrapper), so systemd creates a directory literally called ~ in the working directory, resulting in /home/amnesia/~/.local/share/applications/.
The fix would be to either use %h instead of ~, or wrap the mkdir command in /bin/sh -c like the second ExecStart line does.
Evidence
Issue 2: Monero GUI Now Reports Updates Available
On Tails 7.9, the Monero GUI shows an update notification that was not present on Tails 7.8.1. Historically, Tails’ firewall blocked non-Tor connections, which prevented applications like Monero GUI from reaching their update servers. Something changed in 7.9 that allows this to happen now.
What Changed in Tails 7.9: Reintroduction of Transparent Proxying
Tails 7.9 reintroduced transparent TCP proxying, a feature that was removed back in Tails 0.10 (circa 2012). All the relevant changes were shipped as part of Merge Request !3009 (“Support installing Flatpak apps”), merged into stable via commit 51bd13ff.
The key commit is 452facc8 (“Enable transparent proxying through tor for the live user”), which added the following rules to config/chroot_local-includes/etc/ferm/ferm.conf:
table nat chain OUTPUT {
moduli state (ESTABLISHED RELATED) ACCEPT;
proto tcp dport 9040 REDIRECT to-ports 9040;
}
table filter chain OUTPUT {
moduli state (ESTABLISHED RELATED) ACCEPT;
... existing per-app ACCEPT rules ...
policy DROP;
}
The nat OUTPUT chain now intercepts all outgoing TCP connections from the amnesia user and redirects them to Tor’s TransPort on port 9040 via iptables REDIRECT. Tor’s TransPort then reads the original destination using the SO_ORIGINAL_DST socket option and routes the connection through the Tor network. This means any application that makes a plain TCP connection (without configuring a SOCKS proxy) will now have that connection transparently routed through Tor, rather than being dropped by the firewall.
On Tails 7.8.1 and earlier (since 0.10), the filter OUTPUT chain had a policy DROP with specific ACCEPT rules only for applications configured to use Tor’s SOCKS port (9050). Any application that did not explicitly use SOCKS would have its connections silently dropped.
The test suite commit b6273970 confirms this is a deliberate “reintroduction” of transparent proxying. The commit message from 452facc8 explains the rationale:
“We have no other good way to torify all Flatpaks: torsocks doesn’t work; many applications do not respect the usual environment variables (HTTP_PROXY, ALL_PROXY, etc). When we originally stopped doing transparent proxying in Tails 0.10, the motivation was that we would provide high quality per-application configurations for the applications we shipped, but now that we encourage users to install arbitrary applications via Additional Software Packages and now also Flatpak, that is no longer realistic.”
How This Affects Monero GUI
The Monero GUI updater uses a two-phase update check. The first phase is a DNS TXT record query performed by the core Monero library inside Monero::WalletManager::checkUpdates(). If this DNS query succeeds, it returns a version hash, and the second phase calls Updater::fetchSignedHash() which makes HTTP requests to fetch hashes.txt and hashes.txt.sig from https://web.getmonero.org/.
The relevant code in src/libwalletqt/WalletManager.cpp shows:
void WalletManager::checkUpdatesAsync(
const QString &software, const QString &subdir,
const QString &buildTag, const QString &version) {
m_scheduler.run([this, software, subdir, buildTag, version] {
const auto updateInfo = Monero::WalletManager::checkUpdates(
software.toStdString(), subdir.toStdString(),
buildTag.toStdString().c_str(),
version.toStdString().c_str());
if (!std::get<0>(updateInfo)) {
return; // DNS check failed - silent abort
}
// Only reaches here if DNS succeeded
const QString version = ...;
const QByteArray hashFromDns = ...;
Updater().fetchSignedHash(binaryFilename, hashFromDns, signers);
});
}
The critical point: if the DNS check fails (lines 458-460), the function returns silently with no log output and no notification to the user. The fetchSignedHash() HTTP call is never reached. This means on 7.8.1, if the DNS check was failing, the update check would abort silently and the user would see nothing.
Now, inside fetchSignedHash() in src/qt/updater.cpp, the HTTP fetch uses a locally-created Network object with an empty proxy address:
QByteArray Updater::fetchSignedHash(
const QString &binaryFilename,
const QByteArray &hashFromDns,
QPair<QString, QString> &signers) const {
const Network network; // local instance, empty proxy
std::string hashesTxt = network.get("https://web.getmonero.org/downloads/hashes.txt");
std::string hashesTxtSig = network.get("https://web.getmonero.org/downloads/hashes.txt.sig");
... verify and return ...
}
In src/qt/network.cpp, Network::get() calls set_proxy(m_proxyAddress.toStdString()). Since m_proxyAddress is empty, this sets an empty proxy string on the underlying HTTP client, meaning no explicit SOCKS or HTTP proxy is configured for these requests.
On Tails 7.8.1, these unproxied HTTP connections would be dropped by the firewall’s policy DROP in the filter OUTPUT chain. On Tails 7.9, the nat OUTPUT REDIRECT rule intercepts these connections before they reach the filter chain and redirects them to Tor’s TransPort (9040). Tor then routes them through the Tor network. The connections succeed, the update check completes, and the user sees the notification.
Why the Update Notification Appeared Now
The most likely explanation is that on Tails 7.8.1, the GUI updater’s DNS check was failing silently (aborting at the early return), so the HTTP fetch in fetchSignedHash() was never reached. On Tails 7.9, either the DNS check succeeds (possibly due to changes in how Tor resolves DNS, or it may have been a transient issue on 7.8.1), or some other factor changed. If the DNS check does succeed on 7.9, the HTTP fetch then executes and now succeeds because transparent proxying routes it through Tor, whereas on 7.8.1 it would have been dropped.
Clarification on the DNS TXT warning in logs: The warning “WARNING: no two valid DNS TXT records were received” that appears in Monero GUI logs comes from monerod (the daemon) and relates to the MoneroPulse DNS checkpoint system. This is a completely separate subsystem from the GUI updater’s version check DNS query. The GUI updater’s DNS query goes through Monero::WalletManager::checkUpdates() in the core library and its failure (or success) is not logged at the debug level the user sees. I do not have direct evidence of whether the GUI updater’s DNS query was succeeding or failing on 7.8.1.
Summary
Issue 1 (the ~ directory) is a straightforward bug in a systemd service file where the tilde character is not expanded because systemd does not perform shell expansion on ExecStart commands that are not wrapped in /bin/sh -c. The fix is to replace ~ with %h on the affected line. This is fully source-verified.
Issue 2 (Monero GUI update notifications) is a consequence of Tails 7.9’s reintroduction of transparent TCP proxying via iptables REDIRECT to Tor’s TransPort (port 9040). The Monero GUI updater creates a local Network object with an empty proxy string, meaning its HTTP requests are not explicitly configured to use Tor’s SOCKS port. On 7.8.1, these unproxied connections were silently dropped by the firewall. On 7.9, the REDIRECT rule in the nat OUTPUT chain intercepts them and routes them through Tor via TransPort, causing the update check to succeed.
A nuance I want to be upfront about: I cannot confirm from source code alone whether the GUI updater’s DNS TXT query (phase 1) was succeeding or failing on 7.8.1. The function fails silently with no logging. If it was failing, the HTTP fetch was never reached on either version. If it was succeeding on 7.9 but failing on 7.8.1, the transparent proxying change would explain why the HTTP fetch now succeeds when it reaches that stage. Runtime testing with packet capture tools would be needed to confirm the exact behavior.
If anyone has done live testing with Wireshark, sockstrace, or similar tools on Tails 7.9 to verify the actual network behavior, I would be very interested in seeing those results.