See https://github.com/bcduggan/qrexec-connect
Specifically, (Unix) socket-activated systemd services
I frequently want to allow client qubes to connect to existing (Unix) socket-activated systemd services on service qubes, like
GnuPG's SSH and extra sockets, or a socket created by rclone serve. But I don't want to write a new RPC and corresponding
client service for each systemd service. The contents of each of these would be almost identical.
Instead, I want to create a Qubes RPC "template" program on the service qube and a passthrough systemd service template on the client qube. When I need a new RPC and passthrough service, I want to create instances of the services these templates describe with a few short commands.
Crucially, I also want the template configurations to enable this functionality to be compact and dependency-free on a stock TemplateVM in Qubes OS. I don't want to have to install packages, copy files, or read or write any programming languages beyond shell and systemd unit files. If I had to, I should be able to recreate the configurations with a text editor inside the TemplateVM and a brief reference, like this one.
Assume we've already got the RPC and passthrough service templates installed. Let's implement the classic Split-SSH functionality with this functionality. My goal is to reduce creating a new RPC for Split-SSH to:
user@service:/usr/lib/systemd/user$ sudo ln --symbolic gpg-agent-ssh.socket qubes.SSHAgent.socket
user@service:/usr/lib/systemd/user$ systemd --user daemon-reloaduser@service:~$ sudo ln --symbolic /opt/lib/qubes/qrexec-systemd-socket /etc/qubes-rpc/qubes.SSHAgentAnd reduce creating a new corresponding client service for Split-SSH to:
user@client:~$ systemctl --global enable qrexec-client-vm@qubes.SSHAgent.serviceI should be able to repeat this pattern for any other (Unix) socket-activated service available on the service qube.
We need to create a small program that can act as a Qubes RPC for any Unix-socket-activated systemd service. Then we can create new RPCs with a couple of symlinks.
Create the following script at /opt/lib/qubes/qrexec-systemd-socket in the service qube's TemplateVM:
#!/usr/bin/sh
LC_ALL=C
set -euf
QREXEC_SERVICE_NAME="$(basename ${0})"
SYSTEMD_SOCKET_UNIT="$(systemd-escape ${QREXEC_SERVICE_NAME}).socket"
log () {
echo "${2}" | systemd-cat --identifier="qubes-rpc:${QREXEC_SERVICE_NAME}" --priority="${1}"
}
log info "Starting '${QREXEC_SERVICE_NAME}' called from '$QREXEC_REMOTE_DOMAIN'"
DESTINATION_LISTEN_VALUE="$(\
systemctl --user show --value --property Listen ${SYSTEMD_SOCKET_UNIT} \
| grep '.* (Stream)' \
| head --lines=1 \
)"
if [ -z "${DESTINATION_LISTEN_VALUE}" ]
then
log err "Could not find stream socket"
exit 1
fi
DESTINATION_LISTENSTREAM_SOCKET="${DESTINATION_LISTEN_VALUE% (Stream)}"
log debug "Destination socket: ${DESTINATION_LISTENSTREAM_SOCKET}"
exec socat STDIO UNIX-CONNECT:"${DESTINATION_LISTENSTREAM_SOCKET}"Make the script executable:
user@service:~$ sudo chmod a+x /opt/lib/qubes/qrexec-systemd-socketThe script determines the Qubes RPC it should act as from its own name (${0}). Create symlinks from it to destinations in
/etc/qubes-rpc. To create our Split-SSH RPC:
user@service:~$ sudo ln --symbolic /opt/lib/qubes/qrexec-systemd-socket /etc/qubes-rpc/qubes.SSHAgentThe script looks up the destination Unix socket that corresponds to the acting RPC by the RPC name. We need a way to label a
systemd socket with a Qubes RPC service name. Systemd supports aliases for most units, including sockets. The script will find systemd socket units with the name it was invoked with. Alias gpg-agent's SSH socket:
user@service:/usr/lib/systemd/user$ sudo ln --symbolic gpg-agent-ssh.socket qubes.SSHAgent.socketNB 1: The script doesn't do anything special with arguments, but you can use it as an RPC with an argument.
NB 2: The '+', '@', and other characters are valid POSIX filename characters, but they need to be escaped to use as a systemd
unit name. For example, an RPC named '/etc/qubes-rpc/qubes.Dont@me+arg' will find a systemd socket named
'qubes.Dont\x40me\x2barg.socket'. Use systemd-escape <RPC name> to derive the escaped systemd socket unit name.
NB 3: Systemd supports more than one socket for socket-activated services, and it lists those sockets in undefined order. The script uses the first stream socket it finds.
NB 4: The script assumes the first stream socket is AF_UNIX (socat ... UNIX-CONNECT:...). It will fail if the socket is any
other type, including abstract Unix sockets.
We need to expose the new Qubes RPC as a Unix socket on the client qube. This allows programs on the client qube to use the RPC as if it were a local service.
With systemd, we can create a single template service that allows us to create clients for any Qubes RPC.
Prior to systemd version 256, it might have been possible to implement this functionality as a socket-activated service, but I couldn't find a straightforward way to do that. I'll show two systemd services that accomplish the same functionality: a non-socket-activated service for systemd <256, and a socket-activated service for systemd >=256.
NB 1: At the time of writing, Qubes' latest Debian template was version 12, which shipped with systemd 252 and only supported 254 through backports. Also at the time of writing, Qubes' latest Fedora template was 41, which shipped with systemd 256. Version 256 also supports the non-socket-activated service.
NB 2: The systemd escaping rules decribed above apply to RPC name when creating systemd instances.
Create the following unit file at /usr/lib/systemd/user/qrexec-client-vm@.service in the client qube's TemplateVM:
[Unit]
Description=qrexec-client-vm service
ConditionPathExists=/var/run/qubes-service/%p@%i
[Service]
ExecStart=/usr/bin/systemd-socket-activate --accept --inetd --listen=%t/%p/%i qrexec-client-vm @default %iLoad the new unit:
user@client:~$ systemctl --user daemon-reloadCreate the following unit files in /usr/lib/systemd/user in the client qube's TemplateVM:
qrexec-client-vm@.socket:
[Unit]
Description=qrexec-client-vm service
ConditionPathExists=/var/run/qubes-service/%p@%i
[Socket]
# PassFileDescriptorsToExec directive added in systemd 256
PassFileDescriptorsToExec=true
ListenStream=%t/%p/%iqrexec-client-vm@.service:
[Unit]
Description=qrexec-client-vm service
ConditionPathExists=/var/run/qubes-service/%p@%i
[Service]
Type=exec
ExecStart=/usr/bin/systemd-socket-activate --accept --inetd --listen=%t/%p/%i.aux qrexec-client-vm @default %i
ExecStartPost=/usr/bin/socat FD:3 UNIX-CONNECT:%t/%p/%i.auxLoad the new units:
user@client:~$ systemctl --user daemon-reloadTo create a client for the qubes.SSHAgent RPC on the @default qube, create a new instance of qrexec-client-vm@.service:
user@client:~$ systemctl --global enable qrexec-client-vm@qubes.SSHAgent.serviceTo create a client for the qubes.SSHAgent RPC on the @default qube, create a new instance of qrexec-client-vm@.socket:
user@client:~$ systemctl --global enable qrexec-client-vm@qubes.SSHAgent.serviceOn dom0 or a management qube, enable the new service and restart the client qube:
user@dom0:~$ qvm-service --enable client qrexec-client-vm@qubes.SSHAgent
user@dom0:~$ qvm-shutdown --wait client && qvm-start clientThe client services assume a default service qube for each RPC. A policy for the qubes.SSHAgent RPC for the client and
service qubes would be:
#service_name argument source_qube target_qube action [parameter=value]
qubes.SSHAgent + client @default allow target=service
On the service qube, follow the log for the Split-SSH RPC:
user@service:~$ journalctl --user --follow --identifier="qubes-rpc:qubes.SSHAgent"
Feb 14 16:50:10 service qubes-rpc:qubes.SSHAgent[6743]: Starting 'qubes.SSHAgent' called from 'client'
Feb 14 16:50:10 service qubes-rpc:qubes.SSHAgent[6749]: Destination socket: /run/user/1000/gnupg/S.gpg-agent.sshOn the client qube, show the fingerprints all identities represented by the agent:
user@client:~$ SSH_AUTH_SOCK="${XDG_RUNTIME_DIR}/qrexec-client-vm/qubes.SSHAgent" ssh-add -l
256 SHA256:GMSobMmX3B1zrX0tDT3TTcPT7rKJx4ljVMeXArGP4rk qubes.SSHAgent-test (ED25519)