Subject: [bug#64349] [PATH] Guix service for robust and flexible persistent ssh forwarding

From: Runciter <[email-masked]>
Date: Fri, 24 Nov 2023 00:02:04 +0800

Hi,

I now added a system test in gnu/tests/ssh-tunneler.scm. Very minimal,
for a start, the maionette host only creates a reverse port forwarding
service to itself and tests that the shepherd service starts
successfully.

The test passes for me.

If anyone wants to experiment with the services, the easiest starting
point might be from the example configurations I put in the info manual
at doc/ssh-tunneler.texi.

I'll now go back to working on my insane VPN based off this module. If
there's any interest in integrating the ssh-tunneler module below to the
Guix mainline channel, let me know about any questions or improvement
requests.

>From 664da4eb74ae6970720e055ee2e3c76b452f0f6d Mon Sep 17 00:00:00 2001
Message-ID: <[email-masked]>
From: Runciter <[email-masked]>
Date: Thu, 23 Nov 2023 23:25:40 +0800
Subject: [PATCH] ssh-tunneler services: Guix services daemonizing ssh
 forwardings

M  .gitignore
   Add doc/ssh-tunneler.info to the version control ignored files
M  doc/local.mk
   Add doc/ssh-tunneler.texi to the list of files to be compiled into
   .info manuals
A  doc/ssh-tunneler.texi
   Standalone TexInfo manual for the services.
A  gnu/services/ssh-tunneler.scm
   Guix service module for ssh forwardings of all types:
   - port forwardings
   - reverse port forwardings
   - dynamic forwardings
   - tunnel forwardings
A  gnu/tests/ssh-tunneler.scm
   System test in which a marionette host extends a service creating
   a reverse port forward to itself, tests that it starts successfully

Signed-off-by: Runciter <[email-masked]>
---
 .gitignore                    |   1 +
 doc/local.mk                  |   3 +-
 doc/ssh-tunneler.texi         | 979 ++++++++++++++++++++++++++++++++++
 gnu/services/ssh-tunneler.scm | 837 +++++++++++++++++++++++++++++
 gnu/tests/ssh-tunneler.scm    | 107 ++++
 5 files changed, 1926 insertions(+), 1 deletion(-)
 create mode 100644 doc/ssh-tunneler.texi
 create mode 100644 gnu/services/ssh-tunneler.scm
 create mode 100644 gnu/tests/ssh-tunneler.scm

diff --git a/.gitignore b/.gitignore
index 0f74b5da3d..57086aac7b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -67,6 +67,7 @@
 /doc/stamp-vti
 /doc/version.texi
 /doc/version-*.texi
+/doc/ssh-tunneler.info
 /etc/committer.scm
 /etc/gnu-store.mount
 /etc/guix-daemon.cil
diff --git a/doc/local.mk b/doc/local.mk
index 97f0c3a92a..94c12456cd 100644
--- a/doc/local.mk
+++ b/doc/local.mk
@@ -42,7 +42,8 @@ info_TEXINFOS = %D%/guix.texi			\
   %D%/guix-cookbook.de.texi			\
   %D%/guix-cookbook.fr.texi			\
   %D%/guix-cookbook.ko.texi			\
-  %D%/guix-cookbook.sk.texi
+  %D%/guix-cookbook.sk.texi			\
+  %D%/ssh-tunneler.texi
 
 %C%_guix_TEXINFOS = \
   %D%/contributing.texi \
diff --git a/doc/ssh-tunneler.texi b/doc/ssh-tunneler.texi
new file mode 100644
index 0000000000..3c21bf3c2f
--- /dev/null
+++ b/doc/ssh-tunneler.texi
@@ -0,0 +1,979 @@
+\input texinfo
+@c -*-texinfo-*-
+
+@c %**start of header
+@setfilename ssh-tunneler.info
+@documentencoding UTF-8
+@settitle SSH Tunneler Reference Manual
+@c %**end of header
+
+@set UPDATED 31 October 2023
+@set UPDATED-MONTH October 2023
+@set EDITION 0.1.0
+@set VERSION 0.1.0
+
+@copying
+Copyright @copyright{} 2023 Runciter@*
+
+Permission is granted to copy, distribute and/or modify this document
+under the terms of the GNU Free Documentation License, Version 1.3 or
+any later version published by the Free Software Foundation; with no
+Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts.  A
+copy of the license is included in the section entitled ``GNU Free
+Documentation License''.
+@end copying
+
+@dircategory System administration
+@direntry
+* SSH tunneler: (ssh-tunneler).     Daemonized SSH forwardings for GNU Guix
+@end direntry
+
+@titlepage
+@title SSH Tunneler Reference Manual
+@subtitle Daemonized SSH Forwardings for GNU Guix
+@author Runciter
+
+@page
+@vskip 0pt plus 1filll
+Edition @value{EDITION} @*
+@value{UPDATED} @*
+
+@insertcopying
+@end titlepage
+
+@contents
+
+@c *********************************************************************
+@node Top
+@top SSH Tunneler
+
+This document describes SSH Tunneler version @value{VERSION}, a ssh
+forwarding service written for GNU Guix.
+
+The @code{(gnu packages ssh-tunneler)} module provides Guix services
+extending a root or home shepherd with daemonized client ssh connections
+establishing all types of ssh forwardings:
+
+@table @code
+
+@cindex port forwarding
+@item Port forwarding
+Port forwardings, which can be established using the @command{-L} switch
+of the ssh command, forward connections to a port or socket of the ssh
+client to a port or socket of the sshd, or to a port of another remote
+host whose port becomes reachable through client host's port,
+transported through the sshd host.
+
+@cindex reverse port forwarding
+@item Reverse port forwarding
+Reverse port forwardings, which can be established using the
+@command{-R} switch of the ssh command, forward connections to a port or
+socket of the sshd to a port or socket of the ssh client, or to a port
+of another remote host whose port becomes reachable through the sshd
+host's port, transported through the client host.
+
+@cindex dynamic forwarding
+@item Dynamic forwarding
+Dynamic forwardings, which can be established using the @command{-D}
+switch of the ssh command, expose the sshd as a SOCKS proxy that a
+network program which supports this type of proxying can reach through a
+port of the ssh client.
+
+@cindex TUN device forwarding
+@item TUN device forwarding
+Tun device forwardings, a.k.a. ``ssh tunnels'' in the vernacular, which
+can be established using the @command{-w} switch of the ssh command,
+create a TUN software network device on the ssh client and another such
+device on the sshd, such that all network packets routed through either
+of these TUN device are encrypted and can be de-encrypted by the TUN
+device on the other end of the tunnel, should they be transported there
+through other (ultimately physical) network devices.
+
+@end table
+
+@menu
+* Purpose::
+* Configuration::
+* Shepherd actions::
+* GNU Free Documentation License::
+* Concept Index::
+* Programming Index::
+@end menu
+
+@c *********************************************************************
+@node Purpose
+@chapter Purpose
+
+Apart from the proverbial ease with which its adepts are empowered to
+work-around the firewalls of their place of employment, ssh forwarding
+has several other useful applications, non-exhaustively listed
+here. The services that the @code{(gnu packages ssh-tunneler)} module
+extends are an attempt to make these available to Guix users, easily
+configurable, robustly daemonized, and when needed in their most
+unstoppable form.
+
+@table @code
+
+@cindex remote shell access
+@item Remote shell access
+Through reverse port forwarding, the sshd of a home computer stuck
+behind a dynamic IP router can be made permanently available through a
+chosen port of a VPS. On the shepherd side, ssh-tunneler provides
+features to @command{resurrect} the reverse forwarding in case the
+connection to the VPS is unreliable. @xref{Remote shell access} and
+@ref{Resurrected remote shell access} for example configurations.
+
+@cindex censorship-resistant web browsing
+@item Censorship-resistant web browsing
+Many web browsers support SOCKS v4 or v5 proxies. Any sshd can act as
+such a proxy, nearly out-of-the-box (@pxref{sshd configuration}). The
+ssh-tunneler module can be used to turn a remote sshd into a SOCKS proxy
+reachable through a chosen port of localhost. The proxy host can be a
+VPS with a sshd under the user's full control, or a server from a
+company offering a simple commercial SOCKS proxying service. When the
+proxy host is located outside the area where a local censorship IP
+blacklist is enforced, such censorship is effectively nullified for
+purpose of web browsing. At the time of writing, proxy hosts reached
+exclusively in this way seem to be immune to detection by advanced
+packet-scanning techniques... or at the very least, spared from
+automatic blacklisting. @xref{Dynamic forwarding to a SOCKS v5 proxy}
+for an example configuration.
+
+@cindex VPN
+@item VPN
+When augmented with appropriate network addressing, routing and
+@code{iptables} stances on the client and server side, ssh tunnels can
+support the operation of a VPN. At the time of writing, such
+augmentations probably have to be setup by the user manually or using
+shell scripts, since the ssh-tunneler module only supports the creation
+of the ssh tunnel proper.  @xref{ssh tunnel for a VPN} for an example
+configuration of a service extending a ssh tunnel. There are plans in
+the works to create other services that will enable a set of computers
+all running Guix to unite into a small dynamically addressed VPN.
+
+@cindex stealth VPN
+@item Stealth VPN
+It is possible, albeit in a pretty hackish way, to establish a ssh
+tunnel through an intermediate SOCKS proxy. At the time of writing,
+similar to what is mentioned above, when the proxy host is located
+outside an area where packets are being scanned for VPN connection
+signatures, this method protects the VPN server host and the proxy host
+from being blacklisted. The ssh-tunneler module supports the
+establishment of such ``stealth'' tunnels through a SOCKS
+proxy. @xref{Proxyed ssh tunnel for a stealth VPN} for an example
+configuration.
+
+@end table
+
+@c *********************************************************************
+@node Configuration
+@chapter Configuration
+
+@c *********************************************************************
+@menu
+* Client system configuration::
+* sshd configuration::
+* Configuration examples::
+@end menu
+
+@node Client system configuration
+@section Client system configuration
+
+In order to establish the persistent forwardings, the client has to
+extend a service (@pxref{Services,,, guix, GNU Guix}) from its system
+configuration file (@pxref{Using the Configuration System,,, guix, GNU
+Guix}).
+
+@defvar persistent-ssh-service-type
+This is the type for the service extending the shepherd with a
+daemonized ssh connection. Its value must be an
+@code{ssh-connection-configuration} record.
+
+@end defvar
+
+@deftp {Data Type} ssh-connection-configuration
+This is the configuration record for a ssh connection daemonized by the
+shepherd.
+
+@table @asis
+
+@item @code{shepherd-package} (default @code{shepherd})
+A file-like object. The shepherd package to use
+
+@item @code{ssh-package} (default @code{openssh})
+A file-like object. The openssh package to use.
+
+@item @code{netcat-package} (default @code{netcat-openbsd}))
+A file-like object. The netcat-openbsd package to use.
+
+@item @code{sshpass-package} (default @code{sshpass})
+A file-like object. The sshpass package to use.
+
+@item @code{ineutils-package} (default @code{inetutils})
+A file-like object. The inetutils package to use.
+
+@item @code{procps-package} (default @code{procps})
+A file-like object. The procps package to use.
+
+@item @code{socks-proxy-config} (default @code{(socks-proxy-configuration)})
+A guix record of type @code{socks-proxy-configuration}, configuring
+proxying of the connection opened by the service. See below for the
+record's documentation.
+
+@item @code{id-rsa-file?} (default @code{#t})
+A boolean value. Whether to authenticate to the sshd from a private key
+loaded from a file.
+
+@item @code{id-rsa-file} (default @code{"/root/.ssh/id_rsa"})
+A string. When configured to do so, the path to the private key file to
+load in order to authenticate to the sshd.
+
+@item @code{clear-password?} (default @code{#f})
+A boolean value. Whether to authenticate to the sshd with a clear
+password. Setting this field to @code{#t} is not recommended for
+security, especially on a multi-user machine, among other concerns
+because a password will be written into the Guix store in clear text.
+
+@item @code{sshd-user-password} (default @code{"none"})
+A string. When configured to do so, the clear text password to use to
+authenticate the connection. About security, see the reservations above.
+
+@item @code{sshd-user} (default @code{"root"})
+A string, the UNIX handle of the user to authenticate as on the sshd.
+
+@item @code{sshd-host} (default @code{"127.0.0.1"})
+A string defining an IP address. The IP of the sshd to connect to.
+
+@item @code{sshd-port} (default @code{22})
+An integer. The port used to connect to the sshd.
+
+@item @code{gateway-ports?} (default @code{#t})
+A boolean value. Whether to activate the GatewayPorts switch @emph{on
+the client side}. This is the @emph{ssh_config} GatewayPorts, @emph{not}
+the @emph{sshd_config} GatewayPorts.
+
+@item @code{name-prefix} (default @code{"ssh-forwards"})
+A string. The prefix of the service provision of the shepherd service
+supporting the connection. To this prefix will be by default appended a
+suffix computed from the characteristics of the forwarding(s) configured
+for the connection, and its proxy, if any. The resulting string will be
+converted to a symbol.
+
+@item @code{suffix-name?} (default @code{#t})
+A boolean value. Whether to append an automatically computed suffix to
+the shepherd provision of the service supporting the connection.
+
+@item @code{special-options} (default @code{'()})
+A list of strings. A list of options to add to the ssh command of the
+connection.
+
+@item @code{forwards} (default @code{'()})
+A list of @code{ssh-forward-configuration} records.
+
+@item @code{exit-forward-failure?} (default @code{#t}))
+A boolean value. Whether to active the ExitOnForwardFailure
+ssh configuration switch for the connection.
+
+@item @code{connection-attempts} (default @code{1}))
+An integer. the value assigned to to the ConnectionAttempts ssh
+configuration switch of the connection.
+
+@item @code{local-command?}
+A boolean value. Its default is computed at system reconfiguration
+time. Whether to execute a command locally on the client after
+successfully creating the forwardings of the connection. If the shepherd
+service uses a PID file, which is the default, setting this options to
+@code{#f} will prevent the service from starting successfully.
+
+@item @code{extra-local-commands} (default @code{'()})
+A list of strings. A list of commands to execute locally on the client
+after successfully creating the forwardings of the connection and
+starting the shepherd service.
+
+@item @code{require-networking?} (default @code{#t})
+A boolean value. Whether the @code{networking} service should be
+included in the requirements of the shepherd service of the connection.
+
+@item @code{extra-requires} (default @code{'()})
+A list of symbols. A list of extra requirements for the shepherd service
+of the connection.
+
+@item @code{elogind?} (default @code{#f})
+A boolean value.
+
+@item @code{pid-file?} (default @code{#t})
+A boolean value. Whether the shepherd should use a PID file for the
+service of the connection.
+
+@item @code{pid-folder-override?} (default @code{#f})
+A boolean value. Whether to override the shepherd's global default for
+the folder of the PID file of the service.
+
+@item @code{pid-folder-override} (default @code{"/var/run"})
+A string. When configured to override the shepherd's global default, the
+path to the folder where to store the PID file of the service.
+
+@item @code{timeout-override?} (default @code{#f})
+A boolean value. Whether to override the shepherd's global default for
+the timeout of the service startup.
+
+@item @code{timeout-override} (default @code{5})
+An integer. When configured to override the shepherd's global default,
+the timeout of the service startup.
+
+@item @code{dedicated-log-file?} (default @code{#f})
+A boolean value. Whether the service should log to a dedicated file.
+
+@item @code{log-rotate?} (default @code{#f})
+A boolean value. Whether the dedicated log file of the service should be
+rotated by @command{rottlog}. This is an experimental feature.
+
+@item @code{log-folder-override?} (default @code{#f})
+A boolean value. Whether to override the shepherd's global default for
+the log folder of the service.
+
+@item @code{log-folder-override} (default @code{"/var/run"})
+A string. When configured to override the shepherd's global default, the
+folder where to store the log file of the service.
+
+@item @code{verbosity} (default @code{0})
+An integer between 0 and 3, both included. The verbosity level of the
+ssh command of the connection, equal to the number of times the
+@command{-v} switch of ssh is used.
+
+@item @code{command?} (default @code{#f})
+A boolean value. Whether to execute a command on the sshd host after
+successfully creating the forwardings of the connection.
+
+@item @code{command} (default @code{'()})
+A string. When configured to do so, the command to be executed on the
+sshd after successfully establishing the forwardings of the connection.
+
+@item @code{resurrect-time-spec} (default @code{''(next-minute '(47))})
+A quoted cron job time specification, for which the author would like to
+extend his most sincere apologies to the user. See the default value for
+an example of this field's format. The quoted time specification of the
+cron job extended to @command{resurrect} the service, when configured to
+do so. @pxref{Shepherd actions}.
+
+@item @code{flat-resurrect?} (default @code{#f})
+A boolean value. Whether to @emph{not} recursively @command{resurrect}
+the service.
+
+@item @code{force-resurrect-time-spec} (default @code{''(next-hour '(3))})
+A quoted cron job time specification. Apologies repeated. The quoted
+time specification of the cron job extended to @command{force-resurrect}
+the service, when configured to do so. @pxref{Shepherd actions}.
+
+@item @code{flat-force-resurrect?} (default @code{#f})
+A boolean value. Whether to @emph{not} recursively
+@command{force-resurrect} the service.
+
+@item @code{%cron-resurrect?} (default @code{#f})
+A boolean value. Whether to automatically @command{resurrect} the
+service by means of a cron job.
+
+@item @code{%cron-force-resurrect?} (default @code{#f})
+A boolean value. Whether to automatically @command{force-resurrect} the
+service by means of a cron job.
+
+@item @code{%auto-start?} (default @code{#f})
+A boolean value. Whether to automatically start the service on
+boot. This feature is experimental, and unreliable.
+
+@end table
+@end deftp
+
+@deftp {Data Type} ssh-forward-configuration
+This is the configuration record for one of the forwardings provided by
+a daemonized ssh connection.
+
+@table @asis
+
+@item @code{forward-type} (default @code{'dynamic})
+A symbol which can be @code{'dynamic}, @code{'port},
+@code{'reverse-port} or @code{'tunnel}.
+
+@item @code{entry-type} (default @code{'port})
+A symbol which can be @code{'preset} or @code{'any} when the
+@code{'forward-type} field is @code{'tunnel}, and which can be
+@code{'port} or @code{'socket} otherwise. It is ignored when the
+@code{'forward-type} field is @code{'dynamic.}
+
+@item @code{exit-type} (default @code{'port})
+A symbol which can be @code{'preset} or @code{'any} when the
+@code{forward-type} field is @code{'tunnel}, and which can be
+@code{'port} or @code{'socket} otherwise. It is ignored when the
+@code{forward-type} field is @code{'dynamic}.
+
+@item @code{entry-port} (default @code{8971})
+An integer. When the @code{forward-type} is @code{'dynamic},
+@code{'port} or @code{'reverse-port} and the @code{entry-type} is
+@code{'port}, the port to forward from at the entry of the forwarding.
+
+@item @code{exit-port} (default @code{22})
+An integer. When the @code{forward-type} is @code{'port} or
+@code{'reverse-port} and the @code{exit-type} is @code{'port}, the port
+to forward to at the final destination of the forwarding.
+
+@item @code{entry-socket} (default @code{""}))
+A string. When the @code{forward-type} is @code{'port} or
+@code{'reverse-port} and the @code{entry-type} is @code{'socket}, the
+path to the socket file to forward from at the entry of the
+forwarding. This is an experimental feature.
+
+@item @code{exit-socket} (default @code{""})
+A string. When the @code{forward-type} is @code{'port} or
+@code{'reverse-port} and the @code{exit-type} is @code{'socket}, the path
+to the socket file to forward to at the final destination of the
+forwarding. This is an experimental feature.
+
+@item @code{forward-host} (default @code{"127.0.0.1"})
+A string representing an IP address. The final destination host of the
+forwarding, applicable when the @code{forward-type} is @code{'port} or
+@code{'reverse-port}.
+
+@item @code{entry-tun} (default @code{0})
+An integer. When the @code{forward-type} is @code{'tunnel} and the
+@code{entry-type} is @code{'preset}, the TUN interface
+number (tunX) on the side of the client extending the service.
+
+@item @code{exit-tun} (default @code{0})))
+An integer. When the @code{forward-type} is @code{'tunnel} and the
+@code{exit-type} is @code{'preset}, the TUN interface number (tunX) on
+the sshd side.
+
+@end table
+@end deftp
+
+@deftp {Data Type} socks-proxy-configuration
+This is the configuration record for the SOCKS proxy used by a
+daemonized ssh connection to connect to the sshd.
+
+@table @asis
+
+@item @code{use-proxy?} (default @code{#f})
+A boolean value. Whether to establish the ssh connection configured by
+the parent record through a SOCKS proxy. For as much as the rest of this
+record's documentation may be confusing to the first-time reader
+(sorry), he might feel relieved to note that it is sufficient to set the
+value of this field to @code{#t} to proxy a single daemonized ssh
+connection through a default port of localhost.
+
+@item @code{extend?}
+A boolean value. Whether the ssh connection connection supporting the
+SOCKS proxy should be auto-magically extended as a shepherd service on
+whose provision the ssh connection configured by the parent record will
+depend. While, strictly speaking, the default of the @code{extend?}
+field is computed at Guix system-reconfiguration time, the default
+behavior is to auto-magically extend a shepherd service adequately
+configured to expose the proxy on localhost whenever the user
+configuring the Guix system has elected to use such a proxy through the
+@code{use-proxy?} field of this record. Overriding this default behavior
+is experimental, and can be achieved by explicitly setting the
+@code{extend?} field to @code{#f} in your system configuration.
+
+@item @code{port}
+An integer. Its default is computed at Guix system-reconfiguration
+time. Overriding this default is experimental. The port of localhost to
+use to connect to the SOCKS proxy.
+
+@item @code{dynamic-forward}
+A value which can be @code{#f}, or a Guix record returned by a call to
+@code{ssh-connection-configuration}. In the latter case, this field
+defines the configuration record for the service of type
+@code{persistent-ssh-service-type} that will extend the connection
+supporting the proxy for the connection configured by the parent record,
+if any. The value @code{#f} is probably always the most adequate when
+the connection extended by the parent record will not use a SOCKS proxy,
+and it does not need to be changed by the user. When the user
+configuring a the system extends a SOCKS proxy, he may optionally wish
+to change the value of the @code{dynamic-forward} field from its
+computed default, for example if he wants to use a non-default port, one
+requirement being that it must then be a configuration record for a
+connection creating a dynamic port forward as the first member of its
+list of @code{forwards}.
+
+@end table
+@end deftp
+
+For the user's convenience, macros are provided as helpers to
+instantiate @code{ssh-forward-configuration} records with sane defaults
+preset for the supported types of forwardings. Field values of the
+record @code{ssh-forward-configuration} returned by these macros can be
+changed from their defaults exactly as if instantiating a
+@code{ssh-forward-configuration} record directly.
+
+@defmac dynamic-forward-configuration @dots{}
+
+This macro (included for the sake of completeness) returns an
+@code{ssh-forward-configuration} record with sane defaults set to
+configure a dynamic port forwarding in the service. The defaults of
+fields are actually not changed from a direct call to
+@code{dynamic-forward-configuration}. @pxref{Dynamic forwarding to a
+SOCKS v5 proxy} for an example usage.
+
+@end defmac
+
+@defmac port-forward-configuration @dots{}
+
+This macro returns an @code{ssh-forward-configuration} record with sane
+defaults set to configure a port forwarding in the service. The default
+of the @code{forward-type} field of the returned record is changed to
+@code{'port}, and the default of the @code{entry-port} field of the
+record is chanted to @code{6947}. @xref{Port forwarding example} for an
+example usage.
+
+@end defmac
+
+@defmac reverse-port-forward-configuration @dots{}
+
+This macro returns an @code{ssh-forward-configuration} record with sane
+defaults set to configure a reverse port forwarding in the service. The
+default of the @code{forward-type} field of the returned record is
+changed to @code{'reverse-port}, and the default of the
+@code{entry-port} field of the record is chanted to
+@code{6283}. @xref{Remote shell access} for an example usage.
+
+@end defmac
+
+@defmac tunnel-forward-configuration @dots{}
+
+This macro returns an @code{ssh-forward-configuration} record with sane
+defaults set to configure a tunnel forwarding in the service. The
+default of the @code{forward-type} field of the returned record is
+changed to @code{'tunnel}, the default of the @code{entry-type} field of
+the record is chanted to @code{'any}, and the default of the
+@code{exit-type} field of the record is also changed to @code{'any}
+. @xref{ssh tunnel for a VPN} for an example usage.
+
+@end defmac
+
+@c *********************************************************************
+@node sshd configuration
+@section sshd configuration
+
+Any machine running a sshd can be the server for the connection, it
+doesn't need to be running Guix System.
+
+By default, the client extending the service will connect to the sshd as
+root. Depending on the specifics of your sshd host, you might wish to
+change that in order to improve security. You can do it by setting a
+non-default value for the @code{sshd-user} field of the
+@code{ssh-connection-configuration} record of the client service, and
+probably still enjoy functionality provided you do not want to extend a
+ssh tunnel or to reverse forward a priviledged port of the sshd.
+
+Configuring the sshd should be easy, there are 2 items to consider:
+
+@table @code
+
+@item Authentication
+The service extended by the client must authenticate its connection to
+the sshd. The recommended way is through public key authentication. To
+achieve it, the public key corresponding to the private key configured
+by the @code{ssh-connection-configuration} record of the service must be
+registered as authorized for the user of the sshd host that the
+connection authenticates as. In common situations (but see above), and
+with default values of the @code{id-rsa-file?}, @code{id-rsa-file} and
+@code{sshd-user} fields of the client's configuration record, you should
+be able to authenticate by adding the contents of the
+@code{/root/.ssh/id_rsa.pub} file of the client to the
+@code{/root/.ssh/authorized_keys} file of the sshd. Alternatively, when
+the sshd itself is also extended as a Guix service on the server host,
+Guix provides a nice facility to extend public key authorizations
+(@pxref{Networking Services,,, guix, GNU Guix}).
+
+@item GatewayPorts sshd option
+For most uses of the ssh-tunneler service, it should be either practical
+or necessary to set @code{GatewayPorts=yes} in the configuration of
+sshd, for example by adding this option switch to the @code{sshd_config}
+file.
+
+@end table
+
+@c *********************************************************************
+@node Configuration examples
+@section Configuration examples
+
+This section provides a collection of client configuration examples for
+typical uses of the @code{(gnu packages ssh-tunneler)} module, including
+the applications described in @ref{Purpose}.
+
+Throughout this section, @code{1.2.3.4} is used as a placeholder for the
+IP address of the sshd host to which the client extending a forwarding
+service connects.
+
+@menu
+* Port forwarding example::
+* Remote shell access::
+* Resurrected remote shell access::
+* Dynamic forwarding to a SOCKS v5 proxy::
+* Clear password authentication::
+* ssh tunnel for a VPN::
+* Proxyed ssh tunnel for a stealth VPN::
+@end menu
+
+@c *********************************************************************
+@node Port forwarding example
+@subsection Port forwarding
+
+This example extends a port forwarding service which forwards the Guix
+client's port 1357 to the port 2468 of a third host, which the sshd
+at IP 1.2.3.4 reaches at IP 5.6.7.8.
+
+The daemonized ssh forwarding stance is @command{-L 1357:5.6.7.8:2468}.
+
+@cindex port forwarding, example
+@lisp
+         (ssh-connection-configuration
+          (sshd-user "joe-chip")        ; "root" is the default
+          (sshd-host "1.2.3.4")         ; change to IP of sshd as string
+          (forwards
+           (list (port-forward-configuration
+                  (entry-port 1357)     ; 6947 is the default
+                  (exit-host "5.6.7.8") ; default is the sshd's localhost
+                  (exit-port 2468)))))) ; 22 is the default
+@end lisp
+
+You can start the extended service with the following shell command as
+root:
+
+@example
+herd start ssh-forwards@@port,1357:5.6.7.8:2468
+@end example
+
+@c *********************************************************************
+@node Remote shell access
+@subsection Remote shell access
+
+This example extends a simple reverse port forwarding service, of the
+kind that can be used for remote shell access to the local machine,
+should this machine be firewalled or stuck behind a dynamic IP.
+
+The daemonized ssh forwarding stance is @command{-R 1357:localhost:22}.
+
+@cindex reverse port forwarding, example
+@cindex remote shell access, example
+@lisp
+(service persistent-ssh-service-type
+         (ssh-connection-configuration
+          (sshd-user "joe-chip")      ; "root" is the default
+          (sshd-host "1.2.3.4")       ; change to IP of sshd as string
+          (forwards
+           (list (reverse-port-forward-configuration
+                  (entry-port 1357)   ; 6283 is the default
+                  (exit-port 22)))))) ; 22 is the default
+@end lisp
+
+Note that if the port 1357 of the sshd is not priviledged, it is
+possible to extend a connection to the sshd as a non-root user, such as
+in this example.
+
+You can start the extended service with the following shell command as
+root:
+
+@example
+herd start ssh-forwards@@reverse-port,1357:localhost:22
+@end example
+
+After setting @code{GatewayPorts=yes} on the sshd and starting the
+extended shepherd service on the client, you can start a remote shell
+session on the client through the sshd's IP on port 1357, for example by
+running this command to start a remote shell as the client's UNIX handle
+@code{pat-conley}:
+
+@example
+ssh -p 1357 pat-conley@@1.2.3.4
+@end example
+
+@c *********************************************************************
+@node Resurrected remote shell access
+@subsection Remote shell access
+
+This example defines exactly the same reverse port forwarding service as
+the previous one, @xref{Remote shell access}. As an added feature of the
+service, mcron jobs are extended to improve its robustness. Those are
+especially useful and perhaps @emph{necessary} if you cannot physically
+attend the machine which daemonizes the connection for prolonged periods
+of time.
+
+The daemonized ssh forwarding stance is @command{-R 1357:localhost:22}.
+
+@cindex resurrected reverse port forward, ex.
+@cindex resurrected remote shell access, ex.
+@lisp
+(service persistent-ssh-service-type
+         (ssh-connection-configuration
+          (sshd-user "joe-chip")      ; "root" is the default
+          (sshd-host "1.2.3.4")       ; change to IP of sshd as string
+          (%cron-resurrect? #t)       ; #f is the default
+          (resurrect-time-spec ''(next-minute '(38)))
+          (%cron-force-resurrect? #t) ; #f is the default
+          (force-resurrect-time-spec  ''(next-hour '(3)))
+          (forwards
+           (list (reverse-port-forward-configuration
+                  (entry-port 1357)   ; 6283 is the default
+                  (exit-port 22)))))) ; 22 is the default
+@end lisp
+
+You can start the extended service with the following shell command as
+root:
+
+@example
+herd start ssh-forwards@@reverse-port,1357:localhost:22
+@end example
+
+In this example, the daemonized ssh connection is resurrected at the
+38th minute of every hour and forcefully resurrected at 03:00AM every
+day.
+
+For the sake of the illustration's completeness, 2 mcron jobs are
+extended by the configured example service. If your situation makes
+resurrection desirable, you should probably @code{resurrect} your
+tunneler service(s) with a mcron job. If you have decided to
+@code{resurrect} a service, you should only then consider if you also
+want to @code{force-resurrect} this service by means of a second cron
+job. Forced resurrection can be useful in the event a long-running
+daemonized ssh connection has stopped providing its forwardings.
+
+Resurrecting a started service should be completely innocuous to the
+running service being resurrected and consume only a small amount of
+shepherd run-time. The author considers a frequency of once per hour for
+the mcron job of the @code{resurrect} action to be adequate. By
+contrast, in most situations, it is expected to be counter-productive to
+@code{force-resurrect} too frequently. The author recommends a maximum
+frequency of once a day for forced resurrection.
+
+In the event that you resurrect and/or forcefully resurrect multiple
+services, it might be (tbc) good practice to spread the times at which
+the mcron jobs are performed by a couple of minutes or more.
+
+@c *********************************************************************
+@node Dynamic forwarding to a SOCKS v5 proxy
+@subsection Dynamic forwarding to a SOCKS v5 proxy
+
+This example extends a dynamic forwarding service, making the sshd host
+available as a SOCKS proxy.
+
+The daemonized ssh forwarding stance is @command{-D 1357}.
+
+@cindex dynamic forwarding, example
+@cindex SOCKS proxy, example
+@cindex censorship-resistant browsing, ex.
+@lisp
+(service persistent-ssh-service-type
+         (ssh-connection-configuration
+          (sshd-user "joe-chip")         ; "root" is the default
+          (sshd-host "1.2.3.4")          ; change to IP of sshd as string
+          (forwards
+           (list (dynamic-forward-configuration
+                  (entry-port 1357)))))) ; 8971 is the default
+@end lisp
+
+You can start the extended service with the following shell command as
+root:
+
+@example
+herd start ssh-forwards@@dynamic,1357
+@end example
+
+In graphical web browsers, proxy settings are generally accessible
+through a settings dialog. You would setup a proxy of type @code{SOCKS5}
+or @code{SOCKS v5} with proxy host @code{localhost} on port @code{1357}.
+
+For such a general web browsing use case, you definitely need
+@code{GatewayPorts=yes} to be set for the proxy sshd at 1.2.3.4, for
+example in its @code{sshd_config} file.
+
+@c *********************************************************************
+@node Clear password authentication
+@subsection Clear password authentication
+
+This example defines exactly the same dynamic forwarding as the previous
+one, @xref{Dynamic forwarding to a SOCKS v5 proxy}. The difference is
+that authentication is achieved with a password extended in clear text
+from the Guix service's configuration record, and by wrapping the ssh
+command in a call to the @command{sshpass} program.
+
+This type of configuration might expose a clear password to other users
+of the machine, and is not recommended. In any case, it should be
+reserved for situations where key pair authentication is not available,
+and only when the extended clear password does not protect any
+confidential data.
+
+When the client extending the service is a multi-user machine, this is
+even worse security than merely using the sshpass program from
+command-line, because the clear-text password will end up in the Guix
+store.
+
+The daemonized ssh forwarding stance is @command{-D 1357}.
+
+@cindex clear password, example
+@lisp
+(service persistent-ssh-service-type
+         (ssh-connection-configuration
+          (sshd-user "joe-chip")         ; "root" is the default
+          (sshd-host "1.2.3.4")          ; change to IP of sshd as string
+          (clear-password? #t)           ; what do you think you're doing?
+          (sshd-user-password "12345")   ; here's hoping yours is better
+          (forwards
+           (list (dynamic-forward-configuration
+                  (entry-port 1357)))))) ; 8971 is the default
+@end lisp
+
+You can start the extended service with the following shell command as
+root:
+
+@example
+herd start ssh-forwards@@dynamic,1357
+@end example
+
+@c *********************************************************************
+@node ssh tunnel for a VPN
+@subsection ssh tunnel for a VPN
+
+This example extends a ssh tunnel for purpose of supporting a connection
+to the sshd as a VPN server.
+
+The daemonized ssh forwarding stance is @command{-w any:any}.
+
+@cindex TUN device forwarding, example
+@cindex ssh tunnel, example
+@cindex VPN, example
+@lisp
+(service persistent-ssh-service-type
+         (ssh-connection-configuration
+          (sshd-user "root")    ; "root" is the default
+          (sshd-host "1.2.3.4") ; change to IP of sshd as string
+          (forwards
+           (list (tunnel-forward-configuration)))))
+@end lisp
+
+You can start the extended service with the following shell command as
+root:
+
+@example
+herd start ssh-forwards@@tunnel,any:any
+@end example
+
+@c *********************************************************************
+@node Proxyed ssh tunnel for a stealth VPN
+@subsection Proxyed ssh tunnel for a stealth VPN
+
+This example extends a ssh tunnel through an inferior socks proxy of
+which it also extends the dynamic forward, for purpose of supporting a
+connection to the sshd as a ``stealth'' VPN server.
+
+Under the hood, the daemonized ssh connection uses the command
+@command{nc} from the program @code{netcat-openbsd} to direct packets of
+the connection to the dynamic forward providing access to the SOCKS
+proxy. This dirty hack is known to work at the time of writing.
+
+The ssh stance directing the connection to transmit through the proxy
+should be something like the following, with shell quoting added
+somewhat artificially for clarity to the human reader:
+
+@example
+-o ProxyCommand='/gnu/store/...-netcat-openbsd-x.x-x/bin/nc -X 5 -x localhost:1357 %h %p'
+@end example
+
+The daemonized ssh forwarding stance is @command{-w any:any}.
+
+@cindex stealth TUN device forwarding, ex.
+@cindex stealth ssh tunnel, example
+@cindex stealth VPN, example
+@lisp
+(service persistent-ssh-service-type
+         (ssh-connection-configuration
+          (sshd-user "root")    ; "root" is the default
+          (sshd-host "1.2.3.4") ; change to IP of VPN server sshd as string
+          (forwards
+           (list (tunnel-forward-configuration)))
+          (socks-proxy-config
+           (socks-proxy-configuration
+            (use-proxy? #t)
+            (dynamic-forward
+             (ssh-connection-configuration
+              (sshd-host "1.2.3.4") ; change to IP of proxy sshd as string
+              (forwards
+               (list (dynamic-forward-configuration
+                      (entry-port 1357)))))))))) ; default is 8971
+@end lisp
+
+You can start the extended service with the following shell command as
+root:
+
+@example
+herd start ssh-forwards@@tunnel,any:any@@1357
+@end example
+
+In this example, the SOCKS proxy sshd providing stealth and the VPN
+server sshd are actually one and the same host. In general, you can use
+the same host or have 2 different hosts according to your
+preference. While using 2 hosts might provide more privacy, in the
+author's experience, using the same host as SOCKS proxy and VPN server
+still grants protection from IP blacklisting to the sshd.
+
+Final note of caution: once a host is blacklisted, connecting to that
+same host's VPN server stealthily will obviously not unblacklist
+it. Your luck holds only as long as the server host is @emph{only}
+connected to through a SOCKS proxy and @emph{never} directly by VPN
+clients. Do not underestimate the evils of this world, we are not
+@emph{defeating} censorship, merely @emph{flying under the radar} for a
+little while.
+
+@c *********************************************************************
+@node Shepherd actions
+@chapter Shepherd actions
+
+Shepherd services extended by Guix services of type
+@code{persistent-ssh-service-type} provide 2 special shepherd actions,
+@ref{Jump Start,,, shepherd, The GNU Shepherd Manual} on how to use them
+from command-line.
+
+@table @code
+
+@cindex resurrect, shepherd action
+@item resurrect
+The @code{resurrect} action has no side effects when the service to
+which is belongs is running. Otherwise, and in this order, it will
+@code{enable} the service, by default recursively perform itself on a
+service which provides a dynamic forward that the service uses for
+proxying (if any), then @code{start} the service.
+
+@cindex force-resurrect, shepherd action
+@item force-resurrect
+The @code{force-resurrect} always has side effects which include
+stopping before starting the service to which it belongs when this
+service is started. It therefore @emph{always} causes an interruption of
+connectivity, namely it will @code{enable} the service, @code{stop} the
+service, by default recursively perform itself on a service which
+provides a dynamic forward that the service uses for proxying (if any),
+then @code{start} the service.
+
+@end table
+
+
+@c *********************************************************************
+@node GNU Free Documentation License
+@appendix GNU Free Documentation License
+@cindex license, GNU Free Documentation License
+@include fdl-1.3.texi
+
+@c *********************************************************************
+@node Concept Index
+@unnumbered Concept Index
+@printindex cp
+
+@node Programming Index
+@unnumbered Programming Index
+@syncodeindex tp fn
+@syncodeindex vr fn
+@printindex fn
+
+@bye
+
+@c Local Variables:
+@c ispell-local-dictionary: "american";
+@c End:
diff --git a/gnu/services/ssh-tunneler.scm b/gnu/services/ssh-tunneler.scm
new file mode 100644
index 0000000000..571710e769
--- /dev/null
+++ b/gnu/services/ssh-tunneler.scm
@@ -0,0 +1,837 @@
+;;; Whispers --- Stealth VPN and ssh tunneler
+;;; Copyright © 2023 Runciter <[email-masked]>
+;;;
+;;; This file is part of Whispers.
+;;;
+;;; Whispers is free software; you can redistribute it and/or modify it
+;;; under the terms of the GNU General Public License as published by
+;;; the Free Software Foundation; either version 3 of the License, or (at
+;;; your option) any later version.
+;;;
+;;; Whispers is distributed in the hope that it will be useful, but
+;;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;;; GNU General Public License for more details.
+;;;
+;;; You should have received a copy of the GNU General Public License
+;;; along with Whispers.  If not, see <http://www.gnu.org/licenses/>.
+
+(define-module (gnu services ssh-tunneler)
+  #:use-module (guix gexp)
+  #:use-module (guix records)
+  #:use-module (gnu services)
+  #:use-module (gnu services shepherd)
+  #:use-module (gnu services admin)
+  #:use-module (gnu services mcron)
+  #:use-module (gnu packages base)
+  #:use-module (gnu packages admin)
+  #:use-module (gnu packages linux)
+  #:use-module (gnu packages ssh)
+  #:use-module (gnu home services)
+  #:use-module (gnu home services shepherd)
+  #:export (ssh-connection-configuration
+            make-ssh-connection-configuration
+            ssh-connection-configuration?
+            this-ssh-connection-configuration
+            ssh-connection-configuration-forwards
+            ssh-forward-configuration
+            this-ssh-forward-configuration
+            ssh-forward-configuration?
+            make-ssh-forward-configuration
+            ssh-forward-configuration-entry-port
+            socks-proxy-configuration
+            this-socks-proxy-configuration
+            socks-proxy-configuration?
+            make-socks-proxy-configuration
+            dynamic-forward-configuration
+            port-forward-configuration
+            reverse-port-forward-configuration
+            tunnel-forward-configuration
+            persistent-ssh-name
+            persistent-ssh-service-type
+            home-persistent-ssh-service-type))
+
+(define-record-type* <ssh-connection-configuration>
+  ssh-connection-configuration make-ssh-connection-configuration
+  ssh-connection-configuration?
+  this-ssh-connection-configuration
+  ;; A file-like object.
+  (shepherd-package       ssh-connection-configuration-shepherd-package
+                          (default shepherd))
+  ;; A file-like object.
+  (ssh-package            ssh-connection-configuration-ssh-package
+                          (default openssh))
+  ;; A file-like object.
+  (netcat-package         ssh-connection-configuration-netcat-package
+                          (default netcat-openbsd))
+  ;; A file-like object.
+  (sshpass-package        ssh-connection-configuration-sshpass-package
+                          (default sshpass))
+  ;; A file-like object.
+  (ineutils-package       ssh-connection-configuration-inetutils-package
+                          (default inetutils))
+  ;; A file-like object.
+  (procps-package         ssh-connection-configuration-procps-package
+                          (default procps))
+  ;; A guix record of type <socks-proxy-configuration>.
+  (socks-proxy-config     ssh-connection-configuration-socks-proxy-config
+                          (default (socks-proxy-configuration)))
+  ;; A boolean value.
+  (id-rsa-file?           ssh-connection-configuration-id-rsa-file?
+                          (default #t))
+  ;; A string.
+  (id-rsa-file            ssh-connection-configuration-id-rsa-file
+                          (default "/root/.ssh/id_rsa"))
+  ;; A boolean value.
+  (clear-password?        ssh-connection-configuration-clear-password?
+                          (default #f))
+  ;; A string.
+  (sshd-user-password     ssh-connection-configuration-sshd-user-password
+                          (default "none"))
+  ;; A string.
+  (sshd-user              ssh-connection-configuration-sshd-user
+                          (default "root"))
+  ;; A string.
+  (sshd-host              ssh-connection-configuration-sshd-host
+                          (default "127.0.0.1"))
+  ;; An integer.
+  (sshd-port              ssh-connection-configuration-sshd-port
+                          (default 22))
+  ;; A boolean value.
+  (gateway-ports?         ssh-connection-configuration-gateway-ports?
+                          (default #t))
+  ;; A string.
+  (name-prefix            ssh-connection-configuration-name-prefix
+                          (default "ssh-forwards"))
+  ;; A boolean value.
+  (suffix-name?           ssh-connection-configuration-suffix-name?
+                          (default #t))
+  ;; A list of strings.
+  (special-options        ssh-connection-configuration-special-options
+                          (default (list)))
+  ;; A list of <ssh-forward-configuration> records.
+  (forwards               ssh-connection-configuration-forwards
+                          (default '()))
+  ;; A boolean value.
+  (exit-forward-failure?  ssh-connection-configuration-exit-forward-failure?
+                          (default #t))
+  ;; An integer.
+  (connection-attempts    ssh-connection-configuration-connection-attempts
+                          (default 1))
+  ;; A boolean value.
+  (local-command?         ssh-connection-configuration-local-command?
+                          (default (ssh-connection-configuration-pid-file?
+                                    this-ssh-connection-configuration))
+                          (thunked))
+  ;; A list of strings
+  (extra-local-commands   ssh-connection-configuration-extra-local-commands
+                          (default '()))
+  ;; A boolean value.
+  (require-networking?    ssh-connection-configuration-require-networking?
+                          (default #t))
+  ;; A list of symbols.
+  (extra-requires         ssh-connection-configuration-extra-requires
+                          (default '()))
+  ;; A boolean value.
+  (elogind?               ssh-connection-configuration-elogind?
+                          (default #f))
+  ;; A boolean value.
+  (pid-file?              ssh-connection-configuration-pid-file?
+                          (default #t))
+  ;; A boolean value.
+  (pid-folder-override?   ssh-connection-configuration-pid-folder-override?
+                          (default #f))
+  ;; A string.
+  (pid-folder-override    ssh-connection-configuration-pid-folder-override
+                          (default "/var/run"))
+  ;; A boolean value.
+  (timeout-override?      ssh-connection-configuration-timeout-override?
+                          (default #f))
+  ;; An integer.
+  (timeout-override       ssh-connection-configuration-timeout-override
+                          (default 5))
+  ;; A boolean value.
+  (dedicated-log-file?    ssh-connection-configuration-dedicated-log-file?
+                          (default #f))
+  ;; A boolean value.
+  (log-rotate?            ssh-connection-configuration-log-rotate?
+                          (default #f))
+  ;; A boolean value.
+  (log-folder-override?   ssh-connection-configuration-log-folder-override?
+                          (default #f))
+  ;; A string.
+  (log-folder-override    ssh-connection-configuration-log-folder-override
+                          (default "/var/run"))
+  ;; An integer between 0 and 3, both included.
+  (verbosity               ssh-connection-configuration-verbosity
+                           (default 0))
+  ;; A boolean value.
+  (command?               ssh-connection-configuration-command?
+                          (default #f))
+  ;; A string.
+  (command                ssh-connection-configuration-command
+                          (default '()))
+  ;; A quoted cron job time specification.
+  (resurrect-time-spec    ssh-connection-configuration-resurrect-time-spec
+                          (default ''(next-minute '(47))))
+  ;; A boolean value.
+  (flat-resurrect?        ssh-connection-configuration-flat-resurrect?
+                          (default #f))
+  ;; A quoted cron job time specification.
+  (force-resurrect-time-spec
+   ssh-connection-configuration-force-resurrect-time-spec
+   (default ''(next-hour '(3))))
+  ;; A boolean value.
+  (flat-force-resurrect?  ssh-connection-configuration-flat-force-resurrect?
+                          (default #f))
+  ;; A boolean value.
+  (%cron-resurrect?       ssh-connection-configuration-cron-resurrect?
+                          (default #f))
+  ;; A boolean value.
+  (%cron-force-resurrect? ssh-connection-configuration-cron-force-resurrect?
+                          (default #f))
+  ;; A boolean value.
+  (%auto-start?           ssh-connection-configuration-auto-start?
+                          (default #f)))
+
+(define-record-type* <ssh-forward-configuration>
+  ssh-forward-configuration make-ssh-forward-configuration
+  ssh-forward-configuration?
+  this-ssh-forward-configuration
+  ;; A symbol which can be 'dynamic, 'port, 'reverse-port or 'tunnel
+  (forward-type         ssh-forward-configuration-forward-type
+                        (default 'dynamic))
+  ;; A symbol which can be 'preset or 'any when the 'forward-type field
+  ;; is 'tunnel, and which can be 'port or 'socket otherwise. It is
+  ;; ignored when the 'forward-type field is 'dynamic.
+  (entry-type           ssh-forward-configuration-entry-type
+                        (default 'port))
+  ;; A symbol which can be 'preset or 'any when the 'forward-type field
+  ;; is 'tunnel, and which can be 'port or 'socket otherwise. It is
+  ;; ignored when the 'forward-type field evaluates to 'dynamic.
+  (exit-type            ssh-forward-configuration-exit-type
+                        (default 'port))
+  ;; An integer
+  (entry-port           ssh-forward-configuration-entry-port
+                        (default 8971))
+  ;; An integer
+  (exit-port            ssh-forward-configuration-exit-port
+                        (default 22))
+  ;; A string
+  (entry-socket         ssh-forward-configuration-entry-socket
+                        (default ""))
+  ;; A string
+  (exit-socket          ssh-forward-configuration-exit-socket
+                        (default ""))
+  ;; A string
+  (forward-host         ssh-forward-configuration-exit-host
+                        (default "127.0.0.1"))
+  ;; An integer
+  (entry-tun            ssh-forward-configuration-entry-tun
+                        (default 0))
+  ;; An integer
+  (exit-tun             ssh-forward-configuration-exit-tun
+                        (default 0)))
+
+(define-record-type* <socks-proxy-configuration>
+  socks-proxy-configuration make-socks-proxy-configuration
+  socks-proxy-configuration?
+  this-socks-proxy-configuration
+  ;; A boolean value
+  (use-proxy?           socks-proxy-configuration-use-proxy?
+                        (default #f))
+  ;; A boolean value
+  (extend?              socks-proxy-configuration-extend?
+                        (default (socks-proxy-configuration-use-proxy?
+                                  this-socks-proxy-configuration))
+                        (thunked))
+  ;; An integer
+  (port                 socks-proxy-configuration-port
+                        (default
+                          (if
+                           (socks-proxy-configuration-extend?
+                            this-socks-proxy-configuration)
+                           (ssh-forward-configuration-entry-port
+                            (car
+                             (ssh-connection-configuration-forwards
+                              (socks-proxy-configuration-dynamic-forward
+                               this-socks-proxy-configuration))))
+                           8971))
+                        (thunked))
+  ;; #f, or a guix record returned by a call to
+  ;; (ssh-connection-configuration
+  ;;  (forwards (list (dynamic-forward-configuration ...)))
+  ;;  ...)
+  (dynamic-forward      socks-proxy-configuration-dynamic-forward
+                        (default (if (socks-proxy-configuration-extend?
+                                      this-socks-proxy-configuration)
+                                     (dynamic-forward-configuration)
+                                     #f))
+                        (thunked)))
+
+
+(define-syntax dynamic-forward-configuration
+  (syntax-rules ()
+    ((_ fields ...)
+     (ssh-forward-configuration
+      (inherit
+       (ssh-forward-configuration))
+      fields ...))))
+
+(define-syntax port-forward-configuration
+  (syntax-rules ()
+    ((_ fields ...)
+     (ssh-forward-configuration
+      (inherit
+       (ssh-forward-configuration (forward-type 'port)
+                                  (entry-port 6947)))
+      fields ...))))
+
+(define-syntax reverse-port-forward-configuration
+  (syntax-rules ()
+    ((_ fields ...)
+     (ssh-forward-configuration
+      (inherit
+       (ssh-forward-configuration (forward-type 'reverse-port)
+                                  (entry-port 6283)))
+      fields ...))))
+
+(define-syntax tunnel-forward-configuration
+  (syntax-rules ()
+    ((_ fields ...)
+     (ssh-forward-configuration
+      (inherit
+       (ssh-forward-configuration (forward-type 'tunnel)
+                                  (entry-type 'any)
+                                  (exit-type 'any)))
+      fields ...))))
+
+(define (persistent-ssh-socks-port config)
+  "Returns an integer defining the localhost port that a persistent ssh
+connection can use to establish itself through a socks proxy,
+configurable by CONFIG, a record of the <ssh-connection-configuration>
+type."
+  (socks-proxy-configuration-port
+   (ssh-connection-configuration-socks-proxy-config config)))
+
+(define (persistent-ssh-forward-stance forward-conf)
+  "Returns a string defining one of the forwarding stances of a
+persistent ssh connection, configurable by FORWARD-CONF, a record of the
+<ssh-forward-configuration> type."
+  (let* ((forward-type (ssh-forward-configuration-forward-type forward-conf))
+         (entry-type (ssh-forward-configuration-entry-type forward-conf))
+         (exit-type (ssh-forward-configuration-exit-type forward-conf))
+         (entry-port (ssh-forward-configuration-entry-port forward-conf))
+         (entry-port-str (number->string entry-port))
+         (exit-port (ssh-forward-configuration-exit-port forward-conf))
+         (exit-port-str (number->string exit-port))
+         (entry-socket (ssh-forward-configuration-entry-socket forward-conf))
+         (exit-socket (ssh-forward-configuration-exit-socket forward-conf))
+         (exit-host (ssh-forward-configuration-exit-host forward-conf))
+         (entry-tun (ssh-forward-configuration-entry-tun forward-conf))
+         (entry-tun-str (number->string entry-tun))
+         (exit-tun (ssh-forward-configuration-exit-tun forward-conf))
+         (exit-tun-str (number->string exit-tun)))
+    (cond ((equal? forward-type 'dynamic)
+           (number->string entry-port))
+          ((or (equal? forward-type 'port)
+               (equal? forward-type 'reverse-port))
+           (cond ((equal? entry-type 'port) (string-append entry-port-str
+                                                           ":"
+                                                           exit-host
+                                                           ":"
+                                                           exit-port-str))
+                 ((equal? entry-type 'socket) (string-append entry-socket
+                                                             ":"
+                                                             exit-socket))
+                 (#t #f)))
+          ((equal? forward-type 'tunnel)
+           (string-append (cond ((equal? entry-type 'preset) entry-tun-str)
+                                ((equal? entry-type 'any) "any")
+                                (#t #f))
+                          ":"
+                          (cond ((equal? exit-type 'preset) exit-tun-str)
+                                ((equal? exit-type 'any) "any")
+                                (#t #f))))
+          (#t
+           #f))))
+
+(define (persistent-ssh-forward-switch forward-conf)
+  "Returns a string defining one of the forwarding switches of a
+persistent ssh connection, configurable by FORWARD-CONF, a record of the
+<ssh-forward-configuration> type."
+  (let ((forward-type (ssh-forward-configuration-forward-type forward-conf)))
+    (cond ((equal? forward-type 'dynamic) "-D")
+          ((equal? forward-type 'port) "-L")
+          ((equal? forward-type 'reverse-port) "-R")
+          ((equal? forward-type 'tunnel) "-w")
+          (#t #f))))
+
+(define (persistent-ssh-forward forward-conf)
+  "Returns a list of 2 strings containing the switch and stance of one of the
+forwardings of a persistent ssh connection, configurable by
+FORWARD-CONF, a record of the <ssh-forward-configuration> type."
+  (list (persistent-ssh-forward-switch forward-conf)
+        (persistent-ssh-forward-stance forward-conf)))
+
+(define (persistent-ssh-name-suffix config)
+  "Returns a string defining the suffix part of the shepherd service
+provision of the shepherd service daemonizing a persistent ssh
+connection, configurable by CONFIG, a record of the
+<ssh-connection-configuration> type."
+  (let* ((forwards (ssh-connection-configuration-forwards config))
+         (typer ssh-forward-configuration-forward-type)
+         (typer-str (lambda (forward)
+                      (symbol->string (typer forward))))
+         (stancer persistent-ssh-forward-stance)
+         (socks-rec (ssh-connection-configuration-socks-proxy-config config))
+         (use-socks? (socks-proxy-configuration-use-proxy? socks-rec))
+         (socks-port (socks-proxy-configuration-port socks-rec))
+         (socks-port-str (number->string socks-port))
+         (flat? (ssh-connection-configuration-flat-resurrect? config)))
+    (string-append "@"
+                   (string-join (map (lambda (forward)
+                                       (string-append (typer-str forward)
+                                                      ","
+                                                      (stancer forward)))
+                                     forwards)
+                                "_")
+                   (if use-socks?
+                       (string-append "@"
+                                      socks-port-str)
+                       ""))))
+
+(define (persistent-ssh-name config)
+  "Returns a symbol defining the shpherd service provision of the
+shepherd service daemonizing a persistent ssh connection, configurable
+by CONFIG, a record of the <ssh-connection-configuration> type."
+  (string->symbol
+   (string-append (ssh-connection-configuration-name-prefix config)
+                  (if (ssh-connection-configuration-suffix-name? config)
+                      (persistent-ssh-name-suffix config)
+                      ""))))
+
+(define (persistent-ssh-pid-folder config)
+  "Returns a string defining the path to the folder in which the pid
+file of a persistent ssh connection service is stored by default,
+configurable by CONFIG, a record of the <ssh-connection-configuration>
+type."
+  (cond ((ssh-connection-configuration-pid-folder-override? config)
+         (ssh-connection-configuration-pid-folder-override config))
+        ((ssh-connection-configuration-elogind? config)
+         (string-append "/run/user/" (number->string (getuid))))
+        (else "/var/run")))
+
+(define (persistent-ssh-pid-file-path config)
+  "Returns a string defining the path to the pid file of a persistent
+ssh connection service, configurable by CONFIG, configurable by CONFIG,
+a record of the <ssh-connection-configuration> type."
+  (string-append (persistent-ssh-pid-folder config)
+                 "/"
+                 (symbol->string (persistent-ssh-name config))
+                 ".pid"))
+
+(define (persistent-ssh-log-folder config)
+  "Returns a string defining the path to the folder in which the log
+file of a persistent ssh connection service is stored by default,
+configurable by CONFIG, a record of the <ssh-connection-configuration>
+type."
+  (cond ((ssh-connection-configuration-log-folder-override? config)
+         (ssh-connection-configuration-log-folder-override config))
+        ((ssh-connection-configuration-elogind? config)
+         (string-append "/run/user/" (number->string (getuid))))
+        (else "/var/run")))
+
+(define (persistent-ssh-log-file-path config)
+  "Returns a string defining the path to the log file of a persistent
+ssh connection service, configurable by CONFIG, a record of the
+<ssh-connection-configuration> type."
+  (string-append (persistent-ssh-log-folder config)
+                 "/"
+                 (symbol->string (persistent-ssh-name config))
+                 ".log"))
+
+(define (persistent-ssh-local-command config)
+  "Returns a string defining command executed locally after the forwards
+of a persistent ssh connection service have been succesfully created,
+configurable by CONFIG, a record of the <ssh-connection-configuration>
+type."
+  (let ((procps-package (ssh-connection-configuration-procps-package config))
+        (clear-password? (ssh-connection-configuration-clear-password?
+                          config))
+        (extra-local-commands
+         (ssh-connection-configuration-extra-local-commands
+          config)))
+    (append (list (file-append procps-package
+                               "/bin/ps")
+                  " --no-header --pid $PPID -o "
+                  (if clear-password?
+                      "ppid"
+                      "pid")
+                  " > "
+                  (persistent-ssh-pid-file-path config))
+            (map (lambda (command)
+                   (string-append " && "
+                                  command))
+                 extra-local-commands))))
+
+(define (persistent-ssh-requires config)
+  "Returns a list of symbols defining the other services required as
+dependencies by the shepherd service of a persistent ssh connection,
+configurable by CONFIG, a record of the <ssh-connection-configuration>
+type."
+  (let* ((req-net? (ssh-connection-configuration-require-networking? config))
+         (extra-reqs (ssh-connection-configuration-extra-requires config))
+         (socks-rec (ssh-connection-configuration-socks-proxy-config config))
+         (inferior? (socks-proxy-configuration-extend? socks-rec))
+         (inferior-cnf (socks-proxy-configuration-dynamic-forward socks-rec))
+         (use-socks? (socks-proxy-configuration-use-proxy? socks-rec))
+         (socks-port (socks-proxy-configuration-port socks-rec))
+         (socks-port-str (number->string socks-port))
+         (flat? (ssh-connection-configuration-flat-force-resurrect? config)))
+    (append
+     (if req-net?
+         (list 'networking)
+         (list))
+     extra-reqs
+     (if inferior?
+         (list (persistent-ssh-name inferior-cnf))
+         (if use-socks?
+             (list (string->symbol
+                    ;; FIXME: this just assumes a possible
+                    ;; default name, not always true and not
+                    ;; even the only possible default.
+                    (string-append "ssh-forwards@dynamic,"
+                                   (number->string socks-port))))
+             (list))))))
+
+(define (persistent-ssh-timeout config)
+  "Returns an integer setting the pid file timout of the shepherd
+service daemonizing a persistent ssh connection, configurable by CONFIG,
+a record of the <ssh-connection-configuration> type."
+  (if (ssh-connection-configuration-timeout-override? config)
+      (ssh-connection-configuration-timeout-override config)
+      #~(+ #$(ssh-connection-configuration-connection-attempts config)
+           (default-pid-file-timeout))))
+
+(define (persistent-ssh-constructor-gexp config)
+  "Returns G-exp to a procedure starting the ssh client process of a
+persistent ssh connection, configurable by CONFIG, a record of the
+<ssh-connection-configuration> type."
+  (let* ((sshpass-pkg (ssh-connection-configuration-sshpass-package config))
+         (password (ssh-connection-configuration-sshd-user-password config))
+         (ssh-pkg (ssh-connection-configuration-ssh-package config))
+         (netcat-pkg (ssh-connection-configuration-netcat-package config))
+         (verbosity (ssh-connection-configuration-verbosity config))
+         (eff? (ssh-connection-configuration-exit-forward-failure? config))
+         (tries (ssh-connection-configuration-connection-attempts config))
+         (tries-str (number->string tries))
+         (local-com? (ssh-connection-configuration-local-command? config))
+         (local-com (persistent-ssh-local-command config))
+         (gateway? (ssh-connection-configuration-gateway-ports? config))
+         (socks-rec (ssh-connection-configuration-socks-proxy-config config))
+         (use-socks? (socks-proxy-configuration-use-proxy? socks-rec))
+         (socks-port (socks-proxy-configuration-port socks-rec))
+         (socks-port-str (number->string socks-port))
+         (command? (ssh-connection-configuration-command? config))
+         (command (ssh-connection-configuration-command config))
+         (forwards (ssh-connection-configuration-forwards config))
+         (sshd-port (ssh-connection-configuration-sshd-port config))
+         (sshd-port-str (number->string sshd-port))
+         (id-rsa? (ssh-connection-configuration-id-rsa-file? config))
+         (id-rsa (ssh-connection-configuration-id-rsa-file config))
+         (sshd-user (ssh-connection-configuration-sshd-user config))
+         (sshd-host (ssh-connection-configuration-sshd-host config))
+         (dlf? (ssh-connection-configuration-dedicated-log-file? config))
+         (log-file (persistent-ssh-log-file-path config))
+         (pid-file? (ssh-connection-configuration-pid-file? config))
+         (pid-file (persistent-ssh-pid-file-path config))
+         (timeout (persistent-ssh-timeout config))
+         (special-opt (ssh-connection-configuration-special-options config)))
+    #~(make-forkexec-constructor
+       (append #$(if (ssh-connection-configuration-clear-password? config)
+                     #~(list #$(file-append sshpass-pkg "/bin/sshpass")
+                             "-p"
+                             #$password)
+                     #~(list))
+               (list #$(file-append ssh-pkg "/bin/ssh")
+                     "-o"
+                     "TCPKeepAlive=no"
+                     "-o"
+                     "ServerAliveInterval=30"
+                     "-o"
+                     "ServerAliveCountMax=6"
+                     "-o"
+                     "UserKnownHostsFile=/dev/null"
+                     "-o"
+                     "StrictHostKeyChecking=no"
+                     ;; "-o"
+                     ;; "Tunnel=point-to-point"
+                     "-o"
+                     (string-append "ExitOnForwardFailure="
+                                    #$(if eff?
+                                          "yes"
+                                          "no"))
+                     "-o"
+                     (string-append "ConnectionAttempts="
+                                    #$tries-str))
+               #$(if local-com?
+                     #~(list "-o"
+                             "PermitLocalCommand=yes"
+                             "-o"
+                             (apply string-append
+                                    (append (list "LocalCommand=")
+                                            #$(append (list 'list)
+                                                      local-com))))
+                     #~(list))
+               #$(if gateway?
+                     #~(list "-o"
+                             "GatewayPorts=yes")
+                     #~(list))
+               #$(if use-socks?
+                     #~(list "-o"
+                             (string-append "ProxyCommand="
+                                            #$netcat-pkg
+                                            "/bin/nc"
+                                            " -X 5 -x localhost:"
+                                            #$socks-port-str
+                                            " %h %p"))
+                     #~(list))
+               #$(append (list 'list)
+                         special-opt)
+               (list "-p"
+                     #$sshd-port-str)
+               #$(if id-rsa?
+                     #~(list "-i"
+                             #$id-rsa)
+                     #~(list))
+               #$(cond ((= verbosity 0) #~(list))
+                       ((= verbosity 1) #~(list "-v"))
+                       ((= verbosity 2) #~(list "-v" "-v"))
+                       ((= verbosity 3) #~(list "-v" "-v" "-v"))
+                       (#t #f))
+               #$(if command?
+                     #~(list)
+                     #~(list "-N"))
+               #$(append (list 'list)
+                         (apply append
+                                (map persistent-ssh-forward
+                                     forwards)))
+               (list (string-append #$sshd-user
+                                    "@"
+                                    #$sshd-host))
+               #$(if command?
+                     #~(list #$command)
+                     #~(list)))
+       #:log-file
+       #$(if dlf?
+             log-file
+             #f)
+       #:pid-file
+       #$(if pid-file?
+             pid-file
+             #f)
+       #:pid-file-timeout
+       #$timeout)))
+
+(define (persistent-ssh-resurrect-action config)
+  "Returns a G-exp to a procedure used as the procedure of the
+'resurrect action of the shepherd service supporting a persistent ssh
+connection , configurable by CONFIG, a record of the
+<ssh-connection-configuration> type."
+  (let* ((name (persistent-ssh-name config))
+         (socks-rec (ssh-connection-configuration-socks-proxy-config config))
+         (inferior? (socks-proxy-configuration-extend? socks-rec))
+         (inferior-cnf (socks-proxy-configuration-dynamic-forward socks-rec))
+         (use-socks? (socks-proxy-configuration-use-proxy? socks-rec))
+         (socks-port (socks-proxy-configuration-port socks-rec))
+         (socks-port-str (number->string socks-port))
+         (flat? (ssh-connection-configuration-flat-resurrect? config)))
+    #~(lambda (running)
+        (unless (service-running? (lookup-service '#$name))
+          (perform-service-action (lookup-service '#$name)
+                                  'enable)
+          (unless (or #$flat?
+                      (and (not #$inferior?)
+                           (not #$use-socks?)))
+            (let ((inferior-name
+                   '#$(if inferior?
+                          (persistent-ssh-name inferior-cnf)
+                          (if use-socks?
+                              (string->symbol
+                               ;; FIXME: this just assumes a possible
+                               ;; default name, not always true and not
+                               ;; even the only possible default.
+                               (string-append "ssh-forwards@dynamic,"
+                                              socks-port-str))
+                              'not-a-service))))
+              (perform-service-action (lookup-service inferior-name)
+                                      'resurrect)))
+          (start-service (lookup-service '#$name)))
+        #t)))
+
+(define (persistent-ssh-force-resurrect-action config)
+  "Returns a G-exp to a procedure used as the procedure of the
+'force-resurrect action of the shepherd service supporting a persistent
+ssh connection , configurable by CONFIG, a record of the
+<ssh-connection-configuration> type."
+  (let* ((name (persistent-ssh-name config))
+         (socks-rec (ssh-connection-configuration-socks-proxy-config config))
+         (inferior? (socks-proxy-configuration-extend? socks-rec))
+         (inferior-cnf (socks-proxy-configuration-dynamic-forward socks-rec))
+         (use-socks? (socks-proxy-configuration-use-proxy? socks-rec))
+         (socks-port (socks-proxy-configuration-port socks-rec))
+         (socks-port-str (number->string socks-port))
+         (flat? (ssh-connection-configuration-flat-force-resurrect? config)))
+    #~(lambda (running)
+        (perform-service-action (lookup-service '#$name)
+                                'enable)
+        (stop-service (lookup-service '#$name))
+        (unless (or #$flat?
+                    (and (not #$inferior?)
+                         (not #$use-socks?)))
+          (let ((inferior-name
+                 '#$(if inferior?
+                        (persistent-ssh-name inferior-cnf)
+                        (if use-socks?
+                            (string->symbol
+                             ;; FIXME: this just assumes a possible
+                             ;; default name, not always true and not
+                             ;; even the only possible default.
+                             (string-append "ssh-forwards@dynamic,"
+                                            socks-port-str))
+                            'not-a-service))))
+            (perform-service-action (lookup-service inferior-name)
+                                    'force-resurrect)))
+        (start-service (lookup-service '#$name))
+        #t)))
+
+(define (persistent-ssh-shepherd-services config)
+  "Returns a list of shepherd services handling a ssh client daemon
+connection, configured by CONFIG, a record of the
+<ssh-connection-configuration> type."
+  (let* ((name (persistent-ssh-name config))
+         (socks-rec (ssh-connection-configuration-socks-proxy-config config))
+         (inferior? (socks-proxy-configuration-extend? socks-rec))
+         (inferior-cnf (socks-proxy-configuration-dynamic-forward socks-rec))
+         (use-socks? (socks-proxy-configuration-use-proxy? socks-rec))
+         (socks-port (socks-proxy-configuration-port socks-rec))
+         (socks-port-str (number->string socks-port))
+         (reqs (persistent-ssh-requires config))
+         (constructor-gexp (persistent-ssh-constructor-gexp config))
+         (res-gexp (persistent-ssh-resurrect-action config))
+         (force-res-gexp (persistent-ssh-force-resurrect-action config))
+         (auto-start? (ssh-connection-configuration-auto-start? config)))
+    (append
+     (if inferior?
+         (persistent-ssh-shepherd-services inferior-cnf)
+         (list))
+     (list
+      (shepherd-service
+       (documentation "Persistent ssh client connection")
+       (provision `(,name))
+       (requirement reqs)
+       (start constructor-gexp)
+       (stop #~(make-kill-destructor))
+       (actions
+        (list
+         (shepherd-action (name 'resurrect)
+                          (documentation
+                           "Resurrect this connection and its
+inferiors-proxies if they are stopped or disabled by the Shepherd.")
+                          (procedure res-gexp))
+         (shepherd-action (name 'force-resurrect)
+                          (documentation "Enable, stop and restart this
+connection and its inferior-proxies , regardless of their current
+status.")
+                          (procedure force-res-gexp))))
+       (auto-start? auto-start?))))))
+
+(define (persistent-ssh-cron-jobs config)
+  "Returns a list of cron job specifications to extend the mcron service
+with scheduled resurrection actions on the persistent ssh connection
+port forwards configured by CONFIG, a record of the
+<ssh-connection-configuration> type."
+  (append
+   (if (ssh-connection-configuration-cron-resurrect? config)
+       (list
+        #~(job #$(ssh-connection-configuration-resurrect-time-spec config)
+               (lambda ()
+                 (execl
+                  (string-append
+                   #$(ssh-connection-configuration-shepherd-package config)
+                   "/bin/herd")
+                  "herd"
+                  "resurrect"
+                  #$(symbol->string (persistent-ssh-name config))))
+               (string-append
+                "resurrect "
+                #$(symbol->string (persistent-ssh-name config)))))
+       (list))
+   (if (ssh-connection-configuration-cron-force-resurrect? config)
+       (list
+        #~(job #$(ssh-connection-configuration-force-resurrect-time-spec
+                  config)
+               (lambda()
+                 (execl
+                  (string-append
+                   #$(ssh-connection-configuration-shepherd-package config)
+                   "/bin/herd")
+                  "herd"
+                  "force-resurrect"
+                  #$(symbol->string (persistent-ssh-name config))))
+               (string-append
+                "force-resurrect "
+                #$(symbol->string (persistent-ssh-name config)))))
+       (list))))
+
+(define (persistent-ssh-log-rotation config)
+  "Returns a list of log-rotation records specifying how to rotate the
+logs of a persistent ssh connection configurable by CONFIG, a record of
+the <ssh-connection-configuration> type."
+  (if (and (ssh-connection-configuration-dedicated-log-file? config)
+           (ssh-connection-configuration-log-rotate? config))
+      (list
+       (log-rotation (frequency 'daily)
+                     (files `(,(persistent-ssh-log-file-path config)))))
+      (list)))
+
+(define persistent-ssh-service-type
+  (service-type
+   (name 'persistent-ssh)
+   (description "Persistent ssh connection service")
+   (extensions
+    (list (service-extension shepherd-root-service-type
+                             persistent-ssh-shepherd-services)
+          (service-extension mcron-service-type
+                             persistent-ssh-cron-jobs)
+          (service-extension rottlog-service-type
+                             persistent-ssh-log-rotation)
+          (service-extension
+           profile-service-type
+           (lambda (config)
+             (list
+              (ssh-connection-configuration-ssh-package config)
+              (ssh-connection-configuration-netcat-package config)
+              (ssh-connection-configuration-sshpass-package config)
+              (ssh-connection-configuration-procps-package config)
+              (ssh-connection-configuration-inetutils-package config))))))
+   (default-value (ssh-connection-configuration))))
+
+(define home-persistent-ssh-service-type
+  (service-type
+   (name 'persistent-ssh)
+   (description "Persistent ssh connection normal user service")
+   (extensions
+    (list (service-extension home-shepherd-service-type
+                             persistent-ssh-shepherd-services)
+          (service-extension
+           home-profile-service-type
+           (lambda (config)
+             (list
+              (ssh-connection-configuration-ssh-package config)
+              (ssh-connection-configuration-netcat-package config)
+              (ssh-connection-configuration-sshpass-package config)
+              (ssh-connection-configuration-procps-package config)
+              (ssh-connection-configuration-inetutils-package config))))))
+   (default-value (ssh-connection-configuration))))
diff --git a/gnu/tests/ssh-tunneler.scm b/gnu/tests/ssh-tunneler.scm
new file mode 100644
index 0000000000..edc6b82442
--- /dev/null
+++ b/gnu/tests/ssh-tunneler.scm
@@ -0,0 +1,107 @@
+;;; GNU Guix --- Functional package management for GNU
+;;; Copyright © 2017 Christopher Baines <[email-masked]>
+;;; Copyright © 2018 Clément Lassieur <[email-masked]>
+;;; Copyright © 2021 Ludovic Courtès <[email-masked]>
+;;; Copyright © 2023 Runciter <[email-masked]>
+;;;
+;;; This file is part of GNU Guix.
+;;;
+;;; GNU Guix is free software; you can redistribute it and/or modify it
+;;; under the terms of the GNU General Public License as published by
+;;; the Free Software Foundation; either version 3 of the License, or (at
+;;; your option) any later version.
+;;;
+;;; GNU Guix is distributed in the hope that it will be useful, but
+;;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;;; GNU General Public License for more details.
+;;;
+;;; You should have received a copy of the GNU General Public License
+;;; along with GNU Guix.  If not, see <http://www.gnu.org/licenses/>.
+
+(define-module (gnu tests ssh-tunneler)
+  #:use-module (gnu packages rsync)
+  #:use-module (gnu tests)
+  #:use-module (gnu system)
+  #:use-module (gnu system file-systems)
+  #:use-module (gnu system shadow)
+  #:use-module (gnu system vm)
+  #:use-module (gnu services)
+  #:use-module (gnu services ssh)
+  #:use-module (gnu services ssh-tunneler)
+  #:use-module (guix gexp)
+  #:use-module (guix store)
+  #:export (%test-ssh-tunneler))
+
+(define* (run-ssh-tunneler-test ssh-tunneler-os)
+  "Run tests in SSH-TUNNELER-OS, which has a sshd running."
+  (define os
+    (marionette-operating-system
+     ssh-tunneler-os
+     #:imported-modules '((gnu services herd)
+                          (guix combinators))))
+
+  (define vm
+    (virtual-machine
+     (operating-system os)
+     (port-forwardings '())))
+
+  (define test
+    (with-imported-modules '((gnu build marionette))
+      #~(begin
+          (use-modules (srfi srfi-11) (srfi srfi-64)
+                       (gnu build marionette))
+
+          (define marionette
+            (make-marionette (list #$vm)))
+
+          (test-runner-current (system-test-runner #$output))
+          (test-begin "ssh-tunneler")
+
+          ;; Wait for the forwarding to be established
+          (test-assert "service running"
+            (marionette-eval
+             '(begin
+                (use-modules (gnu services herd))
+
+                (start-service
+                  'ssh-forwards@reverse-port,6283:127.0.0.1:22))
+             marionette))
+
+          ;; (test-equal "Test file not copied to read-only share"
+          ;;   1                                  ;see "EXIT VALUES" in rsync(1)
+          ;;   (marionette-eval
+          ;;    '(status:exit-val
+          ;;      (system* "rsync" "/tmp/input"
+          ;;               (string-append "rsync://localhost:"
+          ;;                              (number->string #$rsync-port)
+          ;;                              "/read-only/input")))
+          ;;    marionette))
+
+          (test-end))))
+
+  (gexp->derivation "ssh-tunneler-test" test))
+
+(define* %ssh-tunneler-os
+  ;; Return operating system under test.
+  (let ((base-os
+         (simple-operating-system
+          (service openssh-service-type
+                   (openssh-configuration
+                    (permit-root-login #t)
+                    (allow-empty-passwords? #t)))
+          (service persistent-ssh-service-type
+                   (ssh-connection-configuration
+                    (extra-requires '(ssh-daemon))
+                    (require-networking? #f)
+                    (forwards
+                     (list (reverse-port-forward-configuration))))))))
+    (operating-system
+      (inherit base-os)
+      (packages (operating-system-packages base-os)))))
+
+(define %test-ssh-tunneler
+  (system-test
+   (name "ssh-tunneler")
+   (description "Test a VM running ssh forwarding services.")
+   (value (run-ssh-tunneler-test %ssh-tunneler-os))))

base-commit: c26ed5d6b971af11894015979f1e260df571a2be
-- 
2.41.0