agent: Implement --supervised command (for systemd, etc).
authorDaniel Kahn Gillmor <dkg@fifthhorseman.net>
Fri, 12 Aug 2016 05:37:59 +0000 (01:37 -0400)
committerWerner Koch <wk@gnupg.org>
Tue, 4 Oct 2016 15:11:43 +0000 (17:11 +0200)
* agent/gpg-agent.c (get_socket_path): New function for POSIX systems
to return the path for a provided unix-domain socket.
(map_supervised_sockets): New function to inspect $LISTEN_FDS and
$LISTEN_FDNAMES and map them to the specific functionality offered by
the agent.
(main): Add --supervised command.  When used, listen on already-open
file descriptors instead of opening our own.
* doc/gpg-agent.texi: Document --supervised option.

--

"gpg-agent --supervised" is a way to invoke gpg-agent such that a
system supervisor like systemd can provide socket-activated startup,
log management, and scheduled shutdown.

When running in this mode, gpg-agent:

 * Does not open its own listening socket; rather, it expects to be
   given a listening socket on incoming file descriptors.

 * Does not detach from the invoking process, staying in the
   foreground instead.  Unless otherwise specified, logs are sent to
   stderr.

Signed-off-by: Daniel Kahn Gillmor <dkg@fifthhorseman.net>
agent/gpg-agent.c
doc/gpg-agent.texi

index e65198a..9221dc3 100644 (file)
@@ -90,6 +90,7 @@ enum cmd_and_opt_values
   oLogFile,
   oServer,
   oDaemon,
+  oSupervised,
   oBatch,
 
   oPinentryProgram,
@@ -152,6 +153,7 @@ static ARGPARSE_OPTS opts[] = {
 
   ARGPARSE_s_n (oDaemon,  "daemon", N_("run in daemon mode (background)")),
   ARGPARSE_s_n (oServer,  "server", N_("run in server mode (foreground)")),
+  ARGPARSE_s_n (oSupervised,  "supervised", N_("run supervised (e.g., systemd)")),
   ARGPARSE_s_n (oVerbose, "verbose", N_("verbose")),
   ARGPARSE_s_n (oQuiet,          "quiet",     N_("be somewhat more quiet")),
   ARGPARSE_s_n (oSh,     "sh",        N_("sh-style command output")),
@@ -730,6 +732,7 @@ thread_init_once (void)
     }
 }
 
+
 static void
 initialize_modules (void)
 {
@@ -742,6 +745,180 @@ initialize_modules (void)
 }
 
 
+/* return a malloc'ed string that is the path to the passed unix-domain socket
+   (or return NULL if this is not a valid unix-domain socket) */
+static char *
+get_socket_path (gnupg_fd_t fd)
+{
+#ifdef HAVE_W32_SYSTEM
+  return NULL;
+#else
+  struct sockaddr_un un;
+  socklen_t len = sizeof(un);
+  char *ret = NULL;
+
+  if (fd == GNUPG_INVALID_FD)
+    return NULL;
+
+  if (getsockname (fd, (struct sockaddr*)&un, &len) != 0)
+    log_error ("could not getsockname(%d) -- error %d (%s)\n", fd,
+               errno, strerror(errno));
+  else if (un.sun_family != AF_UNIX)
+    log_error ("file descriptor %d is not a unix-domain socket\n", fd);
+  else if (len <= offsetof (struct sockaddr_un, sun_path))
+    log_error ("socket path not present for file descriptor %d\n", fd);
+  else if (len > sizeof(un))
+    log_error ("socket path for file descriptor %d was truncated "
+               "(passed %lu bytes, wanted %u)\n", fd, sizeof(un), len);
+  else
+    {
+      log_debug ("file descriptor %d has path %s (%lu octets)\n", fd,
+                 un.sun_path, len - offsetof (struct sockaddr_un, sun_path));
+      ret = malloc(len - offsetof (struct sockaddr_un, sun_path));
+      if (ret == NULL)
+        log_error ("failed to allocate memory for path to file "
+                   "descriptor %d\n", fd);
+      else
+        memcpy (ret, un.sun_path, len);
+    }
+  return ret;
+#endif /* HAVE_W32_SYSTEM */
+}
+
+
+/* Discover which inherited file descriptors correspond to which
+   services/sockets offered by gpg-agent, using the LISTEN_FDS and
+   LISTEN_FDNAMES convention.  The understood labels are "ssh",
+   "extra", and "browser".  Any other label will be interpreted as the
+   standard socket.
+
+   This function is designed to log errors when the expected file
+   descriptors don't make sense, but to do its best to continue to
+   work even in the face of minor misconfigurations.
+
+   For more information on the LISTEN_FDS convention, see
+   sd_listen_fds(3).
+ */
+static void
+map_supervised_sockets (gnupg_fd_t *fd,
+                        gnupg_fd_t *fd_extra,
+                        gnupg_fd_t *fd_browser,
+                        gnupg_fd_t *fd_ssh)
+{
+  const char *listen_pid = NULL;
+  const char *listen_fds = NULL;
+  const char *listen_fdnames = NULL;
+  int listen_fd_count = -1;
+  int listen_fdnames_colons = 0;
+  const char *fdnamep = NULL;
+
+  listen_pid = getenv ("LISTEN_PID");
+  listen_fds = getenv ("LISTEN_FDS");
+  listen_fdnames = getenv ("LISTEN_FDNAMES");
+
+  if (!listen_pid)
+    log_error ("no $LISTEN_PID environment variable found in "
+               "--supervised mode (ignoring).\n");
+  else if (atoi (listen_pid) != getpid ())
+    log_error ("$LISTEN_PID (%d) does not match process ID (%d) "
+               "in --supervised mode (ignoring).\n",
+               atoi (listen_pid), getpid ());
+  else
+    log_debug ("$LISTEN_PID matches process ID (%d)\n",
+               getpid());
+
+  if (listen_fdnames)
+    for (fdnamep = listen_fdnames; *fdnamep; fdnamep++)
+      if (*fdnamep == ':')
+        listen_fdnames_colons++;
+  log_debug ("%d colon(s) in $LISTEN_FDNAMES: (%s)\n", listen_fdnames_colons, listen_fdnames);
+
+  if (!listen_fds)
+    {
+      if (!listen_fdnames)
+        {
+          log_error ("no LISTEN_FDS or LISTEN_FDNAMES environment variables "
+                     "found in --supervised mode (assuming 1 active descriptor).\n");
+          listen_fd_count = 1;
+        }
+      else
+        {
+          log_error ("no LISTEN_FDS environment variable found in --supervised "
+                     " mode (relying on colons in LISTEN_FDNAMES instead)\n");
+          listen_fd_count = listen_fdnames_colons + 1;
+        }
+    }
+  else
+    listen_fd_count = atoi (listen_fds);
+
+  if (listen_fd_count < 1)
+    {
+      log_error ("--supervised mode expects at least one file descriptor (was told %d) "
+                 "(carrying on as though it were 1)\n", listen_fd_count);
+      listen_fd_count = 1;
+    }
+
+  if (!listen_fdnames)
+    {
+      if (listen_fd_count != 1)
+        log_error ("no LISTEN_FDNAMES and LISTEN_FDS (%d) != 1 in --supervised mode. "
+                   "(ignoring all sockets but the first one)\n", listen_fd_count);
+      *fd = 3;
+    }
+  else
+    {
+      int i;
+      if (listen_fd_count != listen_fdnames_colons + 1)
+        {
+          log_fatal ("number of items in LISTEN_FDNAMES (%d) does not match "
+                     "LISTEN_FDS (%d) in --supervised mode\n",
+                     listen_fdnames_colons + 1, listen_fd_count);
+          exit (1);
+        }
+
+      for (i = 3; i < 3 + listen_fd_count; i++)
+        {
+          int found = 0;
+          char *next = strchrnul(listen_fdnames, ':');
+          *next = '\0';
+#define match_socket(var) if (!found && strcmp (listen_fdnames, #var) == 0) \
+            {                                                           \
+              found = 1;                                                \
+              if (*fd_ ## var == GNUPG_INVALID_FD)                      \
+                {                                                       \
+                  *fd_ ## var = i;                                      \
+                  log_info (#var " socket on fd %d\n", i);              \
+                }                                                       \
+              else                                                      \
+                {                                                       \
+                  log_error ("cannot listen on more than one " #var " socket. (closing fd %d)\n", i); \
+                  close (i);                                            \
+                }                                                       \
+            }
+          match_socket(ssh);
+          match_socket(browser);
+          match_socket(extra);
+#undef match_socket
+          if (!found)
+            {
+              if (*fd == GNUPG_INVALID_FD)
+                {
+                  *fd = i;
+                  log_info ("standard socket (\"%s\") on fd %d\n",
+                            listen_fdnames, i);
+                }
+              else
+                {
+                  log_error ("cannot listen on more than one standard socket. (closing fd %d)\n", i);
+                  close (i);
+                }
+            }
+          listen_fdnames = next + 1;
+        }
+    }
+}
+
+
 /* The main entry point.  */
 int
 main (int argc, char **argv )
@@ -757,6 +934,7 @@ main (int argc, char **argv )
   int default_config =1;
   int pipe_server = 0;
   int is_daemon = 0;
+  int is_supervised = 0;
   int nodetach = 0;
   int csh_style = 0;
   char *logfile = NULL;
@@ -954,6 +1132,7 @@ main (int argc, char **argv )
         case oSh: csh_style = 0; break;
         case oServer: pipe_server = 1; break;
         case oDaemon: is_daemon = 1; break;
+        case oSupervised: is_supervised = 1; break;
 
         case oDisplay: default_display = xstrdup (pargs.r.ret_str); break;
         case oTTYname: default_ttyname = xstrdup (pargs.r.ret_str); break;
@@ -1053,9 +1232,9 @@ main (int argc, char **argv )
     bind_textdomain_codeset (PACKAGE_GT, "UTF-8");
 #endif
 
-  if (!pipe_server && !is_daemon && !gpgconf_list)
+  if (!pipe_server && !is_daemon && !gpgconf_list && !is_supervised)
     {
-     /* We have been called without any options and thus we merely
+     /* We have been called without any command and thus we merely
         check whether an agent is already running.  We do this right
         here so that we don't clobber a logfile with this check but
         print the status directly to stderr. */
@@ -1234,6 +1413,54 @@ main (int argc, char **argv )
       agent_deinit_default_ctrl (ctrl);
       xfree (ctrl);
     }
+  else if (is_supervised)
+    {
+      gnupg_fd_t fd = GNUPG_INVALID_FD;
+      gnupg_fd_t fd_extra = GNUPG_INVALID_FD;
+      gnupg_fd_t fd_browser = GNUPG_INVALID_FD;
+      gnupg_fd_t fd_ssh = GNUPG_INVALID_FD;
+
+      /* when supervised and sending logs to stderr, the process
+         supervisor should handle log entry metadata (pid, name,
+         timestamp) */
+      if (!logfile)
+        log_set_prefix (NULL, 0);
+
+      log_info ("%s %s starting in supervised mode.\n",
+                strusage(11), strusage(13) );
+
+      map_supervised_sockets (&fd, &fd_extra, &fd_browser, &fd_ssh);
+      if (fd == GNUPG_INVALID_FD)
+        {
+          log_fatal ("no standard socket provided\n");
+          exit (1);
+        }
+      /* record socket names where possible: */
+      socket_name = get_socket_path (fd);
+      socket_name_extra = get_socket_path (fd_extra);
+      if (socket_name_extra)
+        opt.extra_socket = 2;
+      socket_name_browser = get_socket_path (fd_browser);
+      if (socket_name_browser)
+        opt.browser_socket = 2;
+      socket_name_ssh = get_socket_path (fd_ssh);
+
+#ifdef HAVE_SIGPROCMASK
+      if (startup_signal_mask_valid)
+        {
+          if (sigprocmask (SIG_SETMASK, &startup_signal_mask, NULL))
+            log_error ("error restoring signal mask: %s\n",
+                       strerror (errno));
+        }
+      else
+        log_info ("no saved signal mask\n");
+#endif /*HAVE_SIGPROCMASK*/
+
+      log_debug ("FDs: std: %d extra: %d browser: %d ssh: %d\n",
+                 fd, fd_extra, fd_browser, fd_ssh);
+      handle_connections (fd, fd_extra, fd_browser, fd_ssh);
+      assuan_sock_close (fd);
+    }
   else if (!is_daemon)
     ; /* NOTREACHED */
   else
@@ -1246,6 +1473,8 @@ main (int argc, char **argv )
       pid_t pid;
 #endif
 
+      initialize_modules ();
+
       /* Remove the DISPLAY variable so that a pinentry does not
          default to a specific display.  There is still a default
          display when gpg-agent was started using --display or a
index 0bb7db2..7aacb7b 100644 (file)
@@ -158,6 +158,18 @@ As an alternative you may create a new process as a child of
 gpg-agent: @code{gpg-agent --daemon /bin/sh}.  This way you get a new
 shell with the environment setup properly; after you exit from this
 shell, gpg-agent terminates within a few seconds.
+
+@item --supervised
+@opindex supervised
+Run in the foreground, sending logs by default to stderr, and
+listening on provided file descriptors, which must already be bound to
+listening sockets.  This command is useful when running under systemd
+or other similar process supervision schemes.
+
+In --supervised mode, different file descriptors can be provided for
+use as different socket types (e.g. ssh, extra) as long as they are
+identified in the environment variable $LISTEN_FDNAMES (see
+sd_listen_fds(3) for more information on this convention).
 @end table
 
 @mansect options