Mount/unmount sshfs as network goes up/down

You can use sshfs to mount directories from a file server into your local filesystem. When client and server OS is Linux, this is usually simpler than cifs or nfs mounts.

You need ssh logins without password (using an ssh agent) and the sshfs package:

sudo apt install sshfs

I use two bash scripts to ensure automatic mounting and un-mounting as my network comes up or goes down, especially when using wifi :

Copy the scripts to the following paths (or create symlinks) and use chmod ugo+x to make sure they are executable:

  • /etc/network/if-up.d/mount-sshfs
  • /etc/network/if-post-down.d/unmount-sshfs

If you use NetworkManager you might have to enable and start its dispatcher service:

sudo systemctl enable NetworkManager-dispatcher.service
sudo systemctl start NetworkManager-dispatcher.service

On Debian there is a system script that automatically translates NetworkManager events to ifupdown events:

/etc/NetworkManager/dispatcher.d/01-ifupdown

With the dispatcher and the translater script in place, our mount/unmount scripts will be executed as desired.

User script

Every user who wants to use the mechanism we have set up so far needs to have a personal shell script at

$HOME/.sshfs/mount.sh

For each user, this script contains the sshfs invocations that the user wants to be auto-executed. It will be invoked automatically from /etc/network/if-up.d/mount-sshfs using the user’s permissions. Users who don’t need sshfs mounts, simply don’t create the file at all.

It is up to the individual user to create this file and make it executable.

Here is an example that works well with gnome-keyring as ssh-agent:

#!/bin/bash

# expose env vars for gnome-keyring ssh-agent:
export SSH_AUTH_SOCK="/run/user/$(id -u)/keyring/ssh"
export SSH_AGENT_PID="$(pgrep -f /usr/bin/ssh-agent)"

# if the ssh agent is running:
if [ -n "$SSH_AGENT_PID" ]; then
  sshfs -o idmap=user,ro bubba:/opt/data /home/oliver/hosts/bubba/data
  sshfs -o idmap=user tc: /home/oliver/hosts/tc
fi

We use an Excito Bubba/2 mini server that hosts shared storage for the whole family (bubba:/opt/data) and a little ThinkCentre server where I have a user account (tc:). I mount each of those server locations locally under /home/oliver/hosts.

I mount the shared storage read-only to prevent accidental data loss (-o ro) and my personal files as read-write (-o rw), with ownership mapping by username (idmap=user) to ensure that “oliver” on the server is mapped to the local “oliver”.

Advertisement

Enter passphrase once at X login for ssh, scp, sshfs

I use ssh, scp, sshfs and x2go with key-based authentication, ie. not entering remote passwords when I connect. There are pros and cons of this, but I think it is more secure.

I use a non-empty passphrase but do not want to enter it on every connection. Once per X session is enough for me.

Here is how I set it up on Debian 11 (“bullseye”):

# do this once and set a good passphrase:
ssh-keygen
# then for each of your accounts on remote hosts:
ssh-copy-id username@otherhost

Configure ssh agent and ssh-add to run when your X session starts. I use the gnome-keyring service as agent:

sudo apt install gnome-keyring

In XFCE – Settings – Session and Startup – Application Autostart, I have two entries with trigger “on login” :

  • “SSH Key Agent (GNOME Keyring: SSH Agent)”
  • “ssh-add” – created by me, command: ssh-add

This setup will bring up a visual prompt for your ssh passphrase right after XFCE login. The default ssh-askpass looks quite ugly, so I installed a more modern one:

sudo apt install ssh-askpass-gnome

On Debian, that package sets itself as default ssh-askpass “alternative”. If in doubt , try this:

sudo update-alternatives --config ssh-askpass

The resulting prompt looks like this for me (“Adwaita Dark” theme):

After all this your ssh, scp, sshfs, x2go and other ssh based tools should be able to connect to your remote accounts without password prompts.

Update: I took convenience one step further and enabled “Launch GNOME services on startup” in the Advanced tab of Session and Startup in the XFCE setting, as described in the XFCE wiki.

This activates GNOME Keyring which “is integrated with the user’s login, so that their secret storage can be unlocked when the user logins into their session”. This means it will store your once entered ssh passphrase on disk, using your Linux login as the only secret that you still have to enter (as you log in as usual).

Unicode Emoji Committee 2021 : “Race is Not a Skin Tone. Gender is Not a Haircut”

Yet this idea seems to underly the simplistic 3-dimensional model used to create the new compound emojis shown below that were added to the Unicode standard in 2021:

5 skin tones × 2 haircuts × 2 mouth expressions (kiss/neutral) × 5 skin tones × 2 haircuts
= 10 × 2 × 10 = 200 depictions

Maybe the richness of human love just doesn’t lend itself to be represented like this?

Read more at: https://home.unicode.org/emoji/about-emoji/

👨🏻‍❤️‍👨🏻 👨🏻‍❤️‍👨🏼 👨🏻‍❤️‍👨🏽👨🏻‍❤️‍👨🏾 👨🏻‍❤️‍👨🏿 👨🏼‍❤️‍👨🏻👨🏼‍❤️‍👨🏼 👨🏼‍❤️‍👨🏽 👨🏼‍❤️‍👨🏾👨🏼‍❤️‍👨🏿 👨🏽‍❤️‍👨🏻 👨🏽‍❤️‍👨🏼👨🏽‍❤️‍👨🏽 👨🏽‍❤️‍👨🏾 👨🏽‍❤️‍👨🏿👨🏾‍❤️‍👨🏻 👨🏾‍❤️‍👨🏼 👨🏾‍❤️‍👨🏽👨🏾‍❤️‍👨🏾 👨🏾‍❤️‍👨🏿 👨🏿‍❤️‍👨🏻👨🏿‍❤️‍👨🏼 👨🏿‍❤️‍👨🏽 👨🏿‍❤️‍👨🏾👨🏿‍❤️‍👨🏿 👩🏻‍❤️‍👨🏻 👩🏻‍❤️‍👨🏼👩🏻‍❤️‍👨🏽 👩🏻‍❤️‍👨🏾 👩🏻‍❤️‍👨🏿👩🏻‍❤️‍👩🏻 👩🏻‍❤️‍👩🏼 👩🏻‍❤️‍👩🏽👩🏻‍❤️‍👩🏾 👩🏻‍❤️‍👩🏿 👩🏼‍❤️‍👨🏻👩🏼‍❤️‍👨🏼 👩🏼‍❤️‍👨🏽 👩🏼‍❤️‍👨🏾👩🏼‍❤️‍👨🏿 👩🏼‍❤️‍👩🏻 👩🏼‍❤️‍👩🏼👩🏼‍❤️‍👩🏽 👩🏼‍❤️‍👩🏾 👩🏼‍❤️‍👩🏿👩🏽‍❤️‍👨🏻 👩🏽‍❤️‍👨🏼 👩🏽‍❤️‍👨🏽👩🏽‍❤️‍👨🏾 👩🏽‍❤️‍👨🏿 👩🏽‍❤️‍👩🏻👩🏽‍❤️‍👩🏼 👩🏽‍❤️‍👩🏽 👩🏽‍❤️‍👩🏾👩🏽‍❤️‍👩🏿 👩🏾‍❤️‍👨🏻 👩🏾‍❤️‍👨🏼👩🏾‍❤️‍👨🏽 👩🏾‍❤️‍👨🏾 👩🏾‍❤️‍👨🏿👩🏾‍❤️‍👩🏻 👩🏾‍❤️‍👩🏼 👩🏾‍❤️‍👩🏽👩🏾‍❤️‍👩🏾 👩🏾‍❤️‍👩🏿 👩🏿‍❤️‍👨🏻👩🏿‍❤️‍👨🏼 👩🏿‍❤️‍👨🏽 👩🏿‍❤️‍👨🏾👩🏿‍❤️‍👨🏿 👩🏿‍❤️‍👩🏻 👩🏿‍❤️‍👩🏼👩🏿‍❤️‍👩🏽 👩🏿‍❤️‍👩🏾 👩🏿‍❤️‍👩🏿🧑🏻‍❤️‍🧑🏼 🧑🏻‍❤️‍🧑🏽 🧑🏻‍❤️‍🧑🏾🧑🏻‍❤️‍🧑🏿 🧑🏼‍❤️‍🧑🏻 🧑🏼‍❤️‍🧑🏽🧑🏼‍❤️‍🧑🏾 🧑🏼‍❤️‍🧑🏿 🧑🏽‍❤️‍🧑🏻🧑🏽‍❤️‍🧑🏼 🧑🏽‍❤️‍🧑🏾 🧑🏽‍❤️‍🧑🏿🧑🏾‍❤️‍🧑🏻 🧑🏾‍❤️‍🧑🏼 🧑🏾‍❤️‍🧑🏽🧑🏾‍❤️‍🧑🏿 🧑🏿‍❤️‍🧑🏻 🧑🏿‍❤️‍🧑🏼🧑🏿‍❤️‍🧑🏽 🧑🏿‍❤️‍🧑🏾 👨🏻‍❤️‍💋‍👨🏻👨🏻‍❤️‍💋‍👨🏼 👨🏻‍❤️‍💋‍👨🏽 👨🏻‍❤️‍💋‍👨🏾👨🏻‍❤️‍💋‍👨🏿 👨🏼‍❤️‍💋‍👨🏻 👨🏼‍❤️‍💋‍👨🏼👨🏼‍❤️‍💋‍👨🏽 👨🏼‍❤️‍💋‍👨🏾 👨🏼‍❤️‍💋‍👨🏿👨🏽‍❤️‍💋‍👨🏻 👨🏽‍❤️‍💋‍👨🏼 👨🏽‍❤️‍💋‍👨🏽👨🏽‍❤️‍💋‍👨🏾 👨🏽‍❤️‍💋‍👨🏿 👨🏾‍❤️‍💋‍👨🏻👨🏾‍❤️‍💋‍👨🏼 👨🏾‍❤️‍💋‍👨🏽 👨🏾‍❤️‍💋‍👨🏾👨🏾‍❤️‍💋‍👨🏿 👨🏿‍❤️‍💋‍👨🏻 👨🏿‍❤️‍💋‍👨🏼👨🏿‍❤️‍💋‍👨🏽 👨🏿‍❤️‍💋‍👨🏾 👨🏿‍❤️‍💋‍👨🏿👩🏻‍❤️‍💋‍👨🏻 👩🏻‍❤️‍💋‍👨🏼 👩🏻‍❤️‍💋‍👨🏽👩🏻‍❤️‍💋‍👨🏾 👩🏻‍❤️‍💋‍👨🏿 👩🏻‍❤️‍💋‍👩🏻👩🏻‍❤️‍💋‍👩🏼 👩🏻‍❤️‍💋‍👩🏽 👩🏻‍❤️‍💋‍👩🏾👩🏻‍❤️‍💋‍👩🏿 👩🏼‍❤️‍💋‍👨🏻 👩🏼‍❤️‍💋‍👨🏼👩🏼‍❤️‍💋‍👨🏽 👩🏼‍❤️‍💋‍👨🏾 👩🏼‍❤️‍💋‍👨🏿👩🏼‍❤️‍💋‍👩🏻 👩🏼‍❤️‍💋‍👩🏼 👩🏼‍❤️‍💋‍👩🏽👩🏼‍❤️‍💋‍👩🏾 👩🏼‍❤️‍💋‍👩🏿 👩🏽‍❤️‍💋‍👨🏻👩🏽‍❤️‍💋‍👨🏼 👩🏽‍❤️‍💋‍👨🏽 👩🏽‍❤️‍💋‍👨🏾👩🏽‍❤️‍💋‍👨🏿 👩🏽‍❤️‍💋‍👩🏻 👩🏽‍❤️‍💋‍👩🏼👩🏽‍❤️‍💋‍👩🏽 👩🏽‍❤️‍💋‍👩🏾 👩🏽‍❤️‍💋‍👩🏿👩🏾‍❤️‍💋‍👨🏻 👩🏾‍❤️‍💋‍👨🏼 👩🏾‍❤️‍💋‍👨🏽👩🏾‍❤️‍💋‍👨🏾 👩🏾‍❤️‍💋‍👨🏿 👩🏾‍❤️‍💋‍👩🏻👩🏾‍❤️‍💋‍👩🏼 👩🏾‍❤️‍💋‍👩🏽 👩🏾‍❤️‍💋‍👩🏾👩🏾‍❤️‍💋‍👩🏿 👩🏿‍❤️‍💋‍👨🏻 👩🏿‍❤️‍💋‍👨🏼👩🏿‍❤️‍💋‍👨🏽 👩🏿‍❤️‍💋‍👨🏾 👩🏿‍❤️‍💋‍👨🏿👩🏿‍❤️‍💋‍👩🏻 👩🏿‍❤️‍💋‍👩🏼 👩🏿‍❤️‍💋‍👩🏽👩🏿‍❤️‍💋‍👩🏾 👩🏿‍❤️‍💋‍👩🏿 🧑🏻‍❤️‍💋‍🧑🏼🧑🏻‍❤️‍💋‍🧑🏽 🧑🏻‍❤️‍💋‍🧑🏾 🧑🏻‍❤️‍💋‍🧑🏿🧑🏼‍❤️‍💋‍🧑🏻 🧑🏼‍❤️‍💋‍🧑🏽 🧑🏼‍❤️‍💋‍🧑🏾🧑🏼‍❤️‍💋‍🧑🏿 🧑🏽‍❤️‍💋‍🧑🏻 🧑🏽‍❤️‍💋‍🧑🏼🧑🏽‍❤️‍💋‍🧑🏾 🧑🏽‍❤️‍💋‍🧑🏿 🧑🏾‍❤️‍💋‍🧑🏻🧑🏾‍❤️‍💋‍🧑🏼 🧑🏾‍❤️‍💋‍🧑🏽 🧑🏾‍❤️‍💋‍🧑🏿🧑🏿‍❤️‍💋‍🧑🏻 🧑🏿‍❤️‍💋‍🧑🏼 🧑🏿‍❤️‍💋‍🧑🏽🧑🏿‍❤️‍💋‍🧑🏾

X over RDP with Debian client/server

I wanted to remote desktop from a Debian machine to another Debian machine. Both have Xorg installed, i.e. the target is not a headless server.

Installed xrdp on the target (server) side:
sudo apt install xrdp

Installed xfreerdp on the client side:
sudo apt install freerdp2-x11

Run xfreerdp to connect to xrdp (my target hostname is “basement”):
xfreerdp /v:basement +glyph-cache /f

The “+glyph-cache” is a workaround for https://github.com/neutrinolabs/xrdp/issues/1266. The “/f” is for fullscreen.

Further reading: https://linuxize.com/post/how-to-install-xrdp-on-debian-10/

Undo/Redo in Java using Protostuff serialization and binary diffs

Many applications need Undo/Redo functionality. Commonly used implementation patterns are:

  • Command Pattern
  • Memento Pattern (state snapshots)
  • State diffs

When using the Command Pattern one would encapsulate both the change logic and its reversal in command objects. Undo/Redo is implemented by managing stacks of those objects. This approach has its limitations, for example for changes that are unidirectional in nature, like anything involving randomness, encryption, etc.

State snapshots save the full state of the edited data as object graphs or some representation thereof. This is also called the Memento Pattern. It often uses serialization and typically compression of the object graph to reduce memory use and ensure immutable snapshots that can also be stored out-of-process, if desired.

State diffs are based on the idea of State snapshots, but only store the difference between states. This can vastly reduce memory consumption of your Undo/Redo history. It is based on diffing algorithms that compute the delta between two states (or their memento) and allow Undo/Redo by applying the deltas as patches against a given state. A disadvantage is that jumping to a state involves a whole chain of patch applications. But it is a good approach when the user mainly navigates the Undo/Redo history sequentially.

A highly reusable implementation of Undo/Redo using State Diffs is available at my github account: https://github.com/odoepner/diffing-history

It uses the following Open Source libraries:

  • Protostuff for object graph serialization using runtime schema
  • JavaxDelta for binary diffing and patching

It provides the following features:

  • Unlimited Undo and Redo
  • Can handle any type of Java objects
  • Low memory footprint
  • Straightforward type-safe API
  • Supports stack size listeners
  • Gzip compression for the serialized current state

It is Open Source under the Unlicense.

Usage

The main API is the History interface.
Create an instance of DiffingHistory to get started.
The DiffingHistoryTest calls all History methods and illustrates the API.

Connect to Cisco AnyConnect using Debian buster

My employer uses a Cisco AnyConnect VPN.

Today I set up my Debian GNU/Linux 10 (“buster”) to connect to it, using only Open Source components.

My setup:

  • XFCE desktop
  • network-manager
  • openconnect

To install the required packages:

sudo apt install openconnect network-manager-openconnect-gnome network-manager-gnome curl xmlstarlet

The curl and xmlstarlet packages are used by csd-post.sh, a Cisco Anyconnect CSD wrapper script included with OpenConnect.

Debian 10 by default demands at least TLS 1.3 which caused this error:

error:1425F102:SSL routines:ssl_choose_client_version:unsupported protocol

I fixed it by creating a more relaxed openssl configuration:

sudo cp /etc/ssl/openssl.cnf /etc/ssl/openssl_tls_1_0.cnf
sudo vi /etc/ssl/openssl_tls_1_0.cnf

Change only the “MinProtocol” line towards the end of the file to

MinProtocol = TLSv1.0

Then add a helper script /usr/local/bin/csd-post-tls-1-0.sh to use the relaxed config:

#!/bin/bash
export OPENSSL_CONF="/etc/ssl/openssl_tls_1_0.cnf"
/usr/libexec/openconnect/csd-post.sh "$@"

Then configure your VPN connection through the network-manager applet (you might have to logout/login to let XFCE autostart the systray applet):

Check “Allow Cisco Secure Desktop trojan” and as “CSD Wrapper Script” use /usr/local/bin/csd-post-tls-1-0.sh :

And finally connect:

Stay safe on the web using Firefox and these add-ons

First get Firefox then use it to access the links below and install the respective add-ons.

uBlock Origin” by Raymond Hill
Efficient ad-blocker. Easy on CPU and memory

Privacy Badger” by EFF
Automatically learns to block invisible trackers

HTTPS Everywhere” by EFF
Enforces HTTPS encryption whenever possible, even when opening plain http links

Flagfox” by Dave G
Displays location country flag for current website, site safety check, whois lookup

Decentraleyes” by Thomas Rientjes
Prevents content delivery networks (CDN) from tracking you

Facebook Container” by Mozilla
Prevents Facebook from tracking you around the web

FileWatcherReloadingTrigger for Apache Commons configuration2

Apache Commons configuration2 lets you deal with configurations from many sources and formats.

If you want to be able to edit your config file and have your application pick up the changed config without restart, configuration2 provides a flexible approach, but only a somewhat unsatisfying trigger implementation (PeriodicReloadingTrigger), that uses polling to detect config file changes.

Below is my FileWatcherReloadingTrigger class that uses a WatchService to listen for change events from the underlying filesystem and avoids polling. This way your application can almost immediately use the changed configuration.

It can be used in basically the same way as the PeriodicReloadingTrigger, as described by the configuration2 user guide:

Parameters params = new Parameters();
// Read data from this file
File propertiesFile = new File("config.properties");

ReloadingFileBasedConfigurationBuilder<FileBasedConfiguration> builder =
    new ReloadingFileBasedConfigurationBuilder<FileBasedConfiguration>(PropertiesConfiguration.class)
    .configure(params.fileBased()
        .setFile(propertiesFile));
FileWatcherReloadingTrigger trigger = new FileWatcherReloadingTrigger(builder.getReloadingController(),
    null, propertiesFile.toPath());
trigger.start();

And this is the FileWatcherReloadingTrigger class. It uses slf4j-api for logging:

package org.guppy4j.config;

import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;

import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import org.apache.commons.configuration2.reloading.ReloadingController;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


public final class FileWatcherReloadingTrigger {

	private final Logger logger = LoggerFactory.getLogger(getClass());

	private final ExecutorService executorService;
	private final WatchService watchService;

	private final ReloadingController controller;
	private final Object controllerParameter;

	private final Path configFilePath;

	private Future<?> execution;

	FileWatcherReloadingTrigger(ReloadingController controller, Object controllerParameter, Path configFilePath) {
		Objects.requireNonNull(controller, "Reloading controller must not be null");
		Objects.requireNonNull(configFilePath, "The path for the configuration file must not be null");
		this.configFilePath = configFilePath.toAbsolutePath();
		this.controller = controller;
		this.controllerParameter = controllerParameter;
		try {
			watchService = FileSystems.getDefault().newWatchService();
			this.configFilePath.getParent().register(watchService, ENTRY_MODIFY);
		} catch (IOException e) {
			throw new IllegalStateException("Could not set up WatcherService for config file reloads", e);
		}
		executorService = Executors.newSingleThreadExecutor();
	}

	public synchronized void start() {
		if (executorService.isShutdown()) {
			throw new IllegalStateException("Already shut down");
		}
		if (execution == null || execution.isCancelled()) {
			execution = executorService.submit(this::watchForFileChanges);
			logger.info("Execution started. Watching for file changes of {}", configFilePath);
		}
	}

	public synchronized void stop() {
		if (execution != null && !execution.isCancelled()) {
			execution.cancel(true);
			execution = null;
			logger.info("Execution stopped. No longer watching for file changes of {}", configFilePath);
		}
	}

	private void watchForFileChanges() {
		try {
			for (WatchKey watchKey = watchService.take(); watchKey != null; watchKey = watchService.take()) {
				for (WatchEvent<?> event : watchKey.pollEvents()) {
					if (ENTRY_MODIFY.equals(event.kind())) {
						final Path filename = Path.class.cast(event.context());
						if (filename.equals(configFilePath.getFileName())) {
							controller.checkForReloading(controllerParameter);
						}
					}
				}
				watchKey.reset();
			}
		} catch (InterruptedException e) {
			// nothing to do
		}
	}

	@Override
	public synchronized void shutdown() {
		try {
			stop();
		} catch (RuntimeException e) {
			logger.warn("Exception while shutting down", e);
		}
		try {
			executorService.shutdown();
		} catch (RuntimeException e) {
			logger.warn("Exception while shutting down executorService", e);
		}
		try {
			watchService.close();
		} catch (IOException e) {
			logger.warn("Exception while shutting down watchService", e);
		}
	}
}