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

From: Runciter <[email-masked]>
Date: Wed, 22 Nov 2023 00:08:04 +0800

Runciter <[email-masked]> writes:

Hi,

I've been busy with the job sending me on trips all the time, making it
difficult to progress as I'd have liked to.

I only just finished writing a documentation for the module (gnu
services ssh-tunneler). It comes as a standalone manual.

I will now try to add a test for the module with a marionette, as I said
before.

The following patch add doc/ssh-tunneler.texi to the source tree as well
as a minimal stance to compile it into doc/ssh-tunneler.info, and
making git ignore the compile info manual.

Also in the patch, some very minor changes to
gnu/services/ssh-tunneler.scm, which probably and hopefully shouldn't
break the module.

---
 .gitignore                    |   1 +
 doc/local.mk                  |   3 +-
 doc/ssh-tunneler.texi         | 979 ++++++++++++++++++++++++++++++++++
 gnu/services/ssh-tunneler.scm | 837 +++++++++++++++++++++++++++++
 4 files changed, 1819 insertions(+), 1 deletion(-)
 create mode 100644 doc/ssh-tunneler.texi
 create mode 100644 gnu/services/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..fad6a5ec34
--- /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)     ; 8971 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)   ; 8971 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)   ; 8971 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))))

base-commit: c07a5f050f67fa9054e93479cdda2f298c567460
-- 
2.41.0