Abstract sockets for server and audiod.
authorAndre Noll <maan@tuebingen.mpg.de>
Mon, 12 Jan 2015 00:07:19 +0000 (01:07 +0100)
committerAndre Noll <maan@tuebingen.mpg.de>
Sat, 14 Feb 2015 10:45:26 +0000 (11:45 +0100)
Currently para_server and para_audiod won't start if the socket
specials can not be created, for example because /var/paraslash does
not exist or has insufficient permissions.

The abstract namespace feature for local sockets allows to go
without socket specials, as implemented in this commit. The feature
is a non-portable Linux extension though, so a fallback to pathname
sockets is necessary for other operating systems and for backward
compatibility.

For para_server the situation is simple because the socket is created
by the afs process, and only command handlers are supposed to connect.
Since afs and the command handlers are part of the same executable
(para_server), there are no compatibility issues. Hence we simply
use an abstract socket on Linux and a pathname socket elsewhere.

For para_audiod things are more complicated because two executables
are involved: para_audioc and para_audiod. To allow older versions
of para_audioc to connect to recent versions of para_audiod, we let
audiod listen on one socket of either type. Startup fails only if
neither socket can be created.

As for the implementation, we make use of the fact that it makes
not much sense to call create_local_socket() with permission mode
equal to 0, and that there are no permission modes for abstract
sockets. With the patch applied, specifying the mode as zero instructs
create_local_socket() to create an abstract socket. In this case we
also use the given pathname but prepend a zero byte at the front of
the ->sun_path member of struct sockaddr_un to bind(2) the socket to
an address in the abstract namespace.

On the client side (connect_local_socket()) we first try to connect to
an abstract address and fall back to pathnames sockets on errors. This
change is transparent to the callers of connect_local_socket. Hence
para_audioc and afs command handlers need no modifications at all.

The patch also adds a section on abstract sockets to the user manual.

afs.c
audiod.c
net.c
web/manual.m4

diff --git a/afs.c b/afs.c
index c677b6c..65d6ed9 100644 (file)
--- a/afs.c
+++ b/afs.c
@@ -639,11 +639,15 @@ static int setup_command_socket_or_die(void)
        char *socket_name = conf.afs_socket_arg;
 
        unlink(socket_name);
-       ret = create_local_socket(socket_name,
-               S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IWOTH);
+       ret = create_local_socket(socket_name, 0);
        if (ret < 0) {
-               PARA_EMERG_LOG("%s: %s\n", para_strerror(-ret), socket_name);
-               exit(EXIT_FAILURE);
+               ret = create_local_socket(socket_name,
+                       S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IWOTH);
+               if (ret < 0) {
+                       PARA_EMERG_LOG("%s: %s\n", para_strerror(-ret),
+                               socket_name);
+                       exit(EXIT_FAILURE);
+               }
        }
        socket_fd = ret;
        PARA_INFO_LOG("listening on socket %s (fd %d)\n", socket_name,
index e8a82b5..acf4137 100644 (file)
--- a/audiod.c
+++ b/audiod.c
@@ -155,14 +155,22 @@ static struct status_task status_task_struct;
  */
 static struct status_task *stat_task = &status_task_struct;
 
-/**
- * the task for handling audiod commands
+/*
+ * The task for handling audiod commands.
+ *
+ * We need two listening sockets for backward compability: on Linux systems
+ * fd[0] is an abstract socket (more precisely, a socket bound to an address in
+ * the abstract namespace), and fd[1] is the usual pathname socket. On other
+ * systems, fd[0] is negative, and only the pathname socket is used.
  *
- * \sa struct task, struct sched
+ * For 0.5.x we accept connections on both sockets to make sure that old
+ * para_audioc versions can still connect. New versions use only the abstract
+ * socket. Hence after v0.6.0 we can go back to a single socket, either an
+ * abstract one (Linux) or a pathname socket (all other systems).
  */
 struct command_task {
-       /** the local listening socket */
-       int fd;
+       /** The local listening sockets. */
+       int fd[2];
        /** the associated task structure */
        struct task *task;
 };
@@ -956,10 +964,8 @@ static int parse_stream_args(void)
 }
 
 /* does not unlink socket on errors */
-static int audiod_get_socket(void)
+static void init_local_sockets(struct command_task *ct)
 {
-       int ret;
-
        if (conf.socket_given)
                socket_name = para_strdup(conf.socket_arg);
        else {
@@ -971,13 +977,12 @@ static int audiod_get_socket(void)
        PARA_NOTICE_LOG("local socket: %s\n", socket_name);
        if (conf.force_given)
                unlink(socket_name);
-       ret = create_local_socket(socket_name,
+       ct->fd[0] = create_local_socket(socket_name, 0);
+       ct->fd[1] = create_local_socket(socket_name,
                S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IWOTH);
-       if (ret < 0)
-               goto err;
-       return ret;
-err:
-       PARA_EMERG_LOG("%s\n", para_strerror(-ret));
+       if (ct->fd[0] >= 0 || ct->fd[1] >= 0)
+               return;
+       PARA_EMERG_LOG("%s\n", para_strerror(-ct->fd[1]));
        exit(EXIT_FAILURE);
 }
 
@@ -1010,28 +1015,38 @@ static int signal_post_select(struct sched *s, void *context)
 static void command_pre_select(struct sched *s, void *context)
 {
        struct command_task *ct = context;
-       para_fd_set(ct->fd, &s->rfds, &s->max_fileno);
+       int i;
+
+       for (i = 0; i < 2; i++)
+               if (ct->fd[i] >= 0)
+                       para_fd_set(ct->fd[i], &s->rfds, &s->max_fileno);
 }
 
 static int command_post_select(struct sched *s, void *context)
 {
-       int ret;
+       int ret, i;
        struct command_task *ct = context;
        static struct timeval last_status_dump;
        struct timeval tmp, delay;
-       bool force = true;
+       bool force = false;
 
        ret = task_get_notification(ct->task);
        if (ret < 0)
                return ret;
-       ret = handle_connect(ct->fd, &s->rfds);
-       if (ret < 0) {
-               PARA_ERROR_LOG("%s\n", para_strerror(-ret));
-               if (ret == -E_AUDIOD_TERM) {
-                       task_notify_all(s, -ret);
-                       return ret;
-               }
-       } else if (ret > 0)
+       for (i = 0; i < 2; i++) {
+               if (ct->fd[i] < 0)
+                       continue;
+               ret = handle_connect(ct->fd[i], &s->rfds);
+               if (ret < 0) {
+                       PARA_ERROR_LOG("%s\n", para_strerror(-ret));
+                       if (ret == -E_AUDIOD_TERM) {
+                               task_notify_all(s, -ret);
+                               return ret;
+                       }
+               } else if (ret > 0)
+                       force = true;
+       }
+       if (force == true)
                goto dump;
 
        /* if last status dump was less than 500ms ago, do nothing */
@@ -1048,8 +1063,8 @@ static int command_post_select(struct sched *s, void *context)
        delay.tv_sec = 5;
        delay.tv_usec = 0;
        tv_add(&last_status_dump, &delay, &tmp);
-       if (tv_diff(now, &tmp, NULL) < 0)
-               force = false;
+       if (tv_diff(now, &tmp, NULL) > 0)
+               force = true;
 dump:
        audiod_status_dump(force);
        last_status_dump = *now;
@@ -1058,7 +1073,7 @@ dump:
 
 static void init_command_task(struct command_task *ct)
 {
-       ct->fd = audiod_get_socket(); /* doesn't return on errors */
+       init_local_sockets(ct); /* doesn't return on errors */
 
        ct->task = task_register(&(struct task_info) {
                .name = "command",
diff --git a/net.c b/net.c
index 9698e42..463033b 100644 (file)
--- a/net.c
+++ b/net.c
@@ -834,13 +834,14 @@ int dccp_available_ccids(uint8_t **ccid_array)
  * \return Positive on success, \p -E_NAME_TOO_LONG if \a name is longer
  * than \p UNIX_PATH_MAX.
  */
-static int init_unix_addr(struct sockaddr_un *u, const char *name)
+static int init_unix_addr(struct sockaddr_un *u, const char *name,
+               bool abstract)
 {
-       if (strlen(name) >= UNIX_PATH_MAX)
+       if (strlen(name) + abstract >= UNIX_PATH_MAX)
                return -E_NAME_TOO_LONG;
        memset(u->sun_path, 0, UNIX_PATH_MAX);
        u->sun_family = PF_UNIX;
-       strcpy(u->sun_path, name);
+       strcpy(u->sun_path + abstract, name);
        return 1;
 }
 
@@ -855,6 +856,10 @@ static int init_unix_addr(struct sockaddr_un *u, const char *name)
  * nonblocking mode and listen(2) is called to prepare the socket for
  * accepting incoming connection requests.
  *
+ * If mode is zero, an abstract socket (a non-portable Linux extension) is
+ * created. In this case the socket name has no connection with filesystem
+ * pathnames.
+ *
  * \return The file descriptor on success, negative error code on failure.
  *
  * \sa socket(2), \sa bind(2), \sa chmod(2), listen(2), unix(7).
@@ -863,8 +868,9 @@ int create_local_socket(const char *name, mode_t mode)
 {
        struct sockaddr_un unix_addr;
        int fd, ret;
+       bool abstract = mode == 0;
 
-       ret = init_unix_addr(&unix_addr, name);
+       ret = init_unix_addr(&unix_addr, name, abstract);
        if (ret < 0)
                return ret;
        ret = socket(PF_UNIX, SOCK_STREAM, 0);
@@ -874,14 +880,16 @@ int create_local_socket(const char *name, mode_t mode)
        ret = mark_fd_nonblocking(fd);
        if (ret < 0)
                goto err;
-       ret = bind(fd, (struct sockaddr *)&unix_addr, UNIX_PATH_MAX);
+       ret = bind(fd, (struct sockaddr *)&unix_addr, sizeof(unix_addr));
        if (ret < 0) {
                ret = -ERRNO_TO_PARA_ERROR(errno);
                goto err;
        }
-       ret = -E_CHMOD;
-       if (chmod(name, mode) < 0)
-               goto err;
+       if (!abstract) {
+               ret = -E_CHMOD;
+               if (chmod(name, mode) < 0)
+                       goto err;
+       }
        if (listen(fd , 5) < 0) {
                ret = -ERRNO_TO_PARA_ERROR(errno);
                goto err;
@@ -911,17 +919,22 @@ int connect_local_socket(const char *name)
        int fd, ret;
 
        PARA_DEBUG_LOG("connecting to %s\n", name);
-       ret = init_unix_addr(&unix_addr, name);
-       if (ret < 0)
-               return ret;
        fd = socket(PF_UNIX, SOCK_STREAM, 0);
        if (fd < 0)
                return -ERRNO_TO_PARA_ERROR(errno);
-       if (connect(fd, (struct sockaddr *)&unix_addr, sizeof(unix_addr)) == -1) {
-               ret = -ERRNO_TO_PARA_ERROR(errno);
+       /* first try (linux-only) abstract socket */
+       ret = init_unix_addr(&unix_addr, name, true);
+       if (ret < 0)
                goto err;
-       }
-       return fd;
+       if (connect(fd, (struct sockaddr *)&unix_addr, sizeof(unix_addr)) != -1)
+               return fd;
+       /* next try pathname socket */
+       ret = init_unix_addr(&unix_addr, name, false);
+       if (ret < 0)
+               goto err;
+       if (connect(fd, (struct sockaddr *)&unix_addr, sizeof(unix_addr)) != -1)
+               return fd;
+       ret = -ERRNO_TO_PARA_ERROR(errno);
 err:
        close(fd);
        return ret;
index 0a5f580..0182c51 100644 (file)
@@ -2145,6 +2145,35 @@ maintain state for each listening receiver, multicast often implies
 connectionless transport, which is the reason that it is currently
 only available via UDP.
 
+Abstract socket namespace
+~~~~~~~~~~~~~~~~~~~~~~~~~
+UNIX domain sockets are a traditional way to communicate between
+processes on the same machine. They are always reliable (see above)
+and don't reorder datagrams. Unlike TCP and UDP, UNIX domain sockets
+support passing open file descriptors or process credentials to
+other processes.
+
+The usual way to set up a UNIX domain socket (as obtained from
+socket(2)) for listening is to first bind the socket to a file system
+pathname and then call listen(2), then accept(2). Such sockets are
+called _pathname sockets_ because bind(2) creates a special socket
+file at the specified path. Pathname sockets allow unrelated processes
+to communicate with the listening process by binding to the same path
+and calling connect(2).
+
+There are two problems with pathname sockets:
+
+       * The listing process must be able to (safely) create the
+       socket special in a directory which is also accessible to
+       the connecting process.
+
+       * After an unclean shutdown of the listening process, a stale
+       socket special may reside on the file system.
+
+The abstract socket namespace is a non-portable Linux feature which
+avoids these problems. Abstract sockets are still bound to a name,
+but the name has no connection with file system pathnames.
+
 License
 ~~~~~~~