]> git.tuebingen.mpg.de Git - paraslash.git/commitdiff
Merge branch 'refs/heads/t/listen-address'
authorAndre Noll <maan@tuebingen.mpg.de>
Mon, 28 May 2018 16:29:06 +0000 (18:29 +0200)
committerAndre Noll <maan@tuebingen.mpg.de>
Mon, 28 May 2018 16:31:26 +0000 (18:31 +0200)
A short series which adds options for para_server that allow the user
to specify the listening addresses of the passive sockets.

The resolution of the conflicts in server.c and send_common.c have
been well tested and were cooking for a couple of weeks.

* refs/heads/t/listen-address:
  server: Add --http-listen-address and --dccp-listen-address.
  server: Implement --listen-address for control service.

NEWS.md
dccp_send.c
http_send.c
m4/lls/server.suite.m4
net.c
net.h
send.h
send_common.c
server.c

diff --git a/NEWS.md b/NEWS.md
index d193a1a3072ebc0d71f53691fc589fba3643f04d..4a837ed8fe77e42ee889757c17805bb8addf9e7e 100644 (file)
--- a/NEWS.md
+++ b/NEWS.md
@@ -15,6 +15,9 @@ NEWS
   lines.
 - It is now possible to switch to a different afs database by changing
   the server configuration and sending SIGHUP to the server process.
+- New server options: --listen--address, --http-listen-address and
+  --dccp-listen-address. These options restrict the set of listening
+  addresses of the TCP and DCCP sockets of the server process.
 
 Download: [tarball](./releases/paraslash-git.tar.xz)
 
index 770ac601047a892025724bbb7e677a7d8bcaed18..496895a509f15466e7430af454c784a09bc3ce7e 100644 (file)
@@ -39,8 +39,11 @@ struct dccp_fec_client {
 static void dccp_pre_select(int *max_fileno, fd_set *rfds,
                __a_unused fd_set *wfds)
 {
-       if (dss->listen_fd >= 0)
-               para_fd_set(dss->listen_fd, rfds, max_fileno);
+       unsigned n;
+
+       FOR_EACH_LISTEN_FD(n, dss)
+               if (dss->listen_fds[n] >= 0)
+                       para_fd_set(dss->listen_fds[n], rfds, max_fileno);
 }
 
 /**
@@ -225,6 +228,7 @@ static char *dccp_status(void)
 static void dccp_send_init(void)
 {
        init_sender_status(dss, OPT_RESULT(DCCP_ACCESS),
+               OPT_RESULT(DCCP_LISTEN_ADDRESS),
                OPT_UINT32_VAL(DCCP_PORT), OPT_UINT32_VAL(DCCP_MAX_CLIENTS),
                OPT_GIVEN(DCCP_DEFAULT_DENY));
        generic_com_on(dss, IPPROTO_DCCP);
index 4d612285aeff00f7d8d153be5c9f1743f3fa9425..330b45ac1b3ec675a497796be614c07c0bbd9c36 100644 (file)
@@ -198,10 +198,13 @@ static void http_post_select(fd_set *rfds, __a_unused fd_set *wfds)
 static void http_pre_select(int *max_fileno, fd_set *rfds, fd_set *wfds)
 {
        struct sender_client *sc, *tmp;
+       unsigned n;
 
-       if (hss->listen_fd < 0)
-               return;
-       para_fd_set(hss->listen_fd, rfds, max_fileno);
+       FOR_EACH_LISTEN_FD(n, hss) {
+               if (hss->listen_fds[n] < 0)
+                       continue;
+               para_fd_set(hss->listen_fds[n], rfds, max_fileno);
+       }
        list_for_each_entry_safe(sc, tmp, &hss->client_list, node) {
                struct private_http_sender_data *phsd = sc->private_data;
                if (phsd->status == HTTP_CONNECTED) /* need to recv get request */
@@ -248,6 +251,7 @@ static char *http_status(void)
 static void http_send_init(void)
 {
        init_sender_status(hss, OPT_RESULT(HTTP_ACCESS),
+               OPT_RESULT(HTTP_LISTEN_ADDRESS),
                OPT_UINT32_VAL(HTTP_PORT), OPT_UINT32_VAL(HTTP_MAX_CLIENTS),
                OPT_GIVEN(HTTP_DEFAULT_DENY));
        if (OPT_GIVEN(HTTP_NO_AUTOSTART))
index 6ffdc0078ad15bc02043dda9cd953fb758347ac5..be8f02f5fa4bda5101f0b2ffa67f5e4332ec0b64 100644 (file)
@@ -34,18 +34,45 @@ version-string = GIT_VERSION()
        m4_include(log-timing.m4)
        m4_include(color.m4)
        m4_include(per-command-options-section.m4)
+       [option listen-address]
+               summary = local listening addresses for the control service
+               arg_info = required_arg
+               arg_type = string
+               typestr = addr
+               flag multiple
+               [help]
+                       para_server listens on a TCP socket for incoming connections from
+                       para_client or para_audiod. This option controls on which addresses
+                       the server should listen. If the option is not given, the server
+                       listens on all local addresses (INADDR_ANY for IPv4 addresses,
+                       IN6ADDR_ANY_INIT for IPv6 addresses).
+
+                       The argument specifies an IPv4 or an IPv6 address, either a numerical
+                       network address (for IPv4, numbers-and-dots notation as supported
+                       by inet_aton(3); for IPv6, hexadecimal string format as supported
+                       by inet_pton(3)), or a network hostname, whose network addresses is
+                       looked up and resolved. The address can optionally include a port
+                       number. For addresses for which no port number is given, the argument
+                       of the --port option (see below) is implied.
+
+                       This option may be given multiple times. The server will then listen
+                       on each of the specified addresses.
+
+                       Examples: 10.10.1.1, 10.10.1.2:2991, localhost, localhost:2991,
+                       [::1]:2991, [badc0de::1].
+               [/help]
        [option port]
                short_opt = p
-               summary = listening port of the paraslash control service
+               summary = listening port of the control service
                arg_info = required_arg
                arg_type = uint32
                typestr = portnumber
                default_val = 2990
                [help]
-                       para_server listens on this TCP port for incoming connections
-                       from clients such as para_client. If the default port is changed,
-                       the corresponding option of para_client must be used to connect
-                       to para_server.
+                       This option applies only to addresses given to --listen-address
+                       (see above) which do no include a port number. If the default port
+                       is changed, the corresponding option of para_client must be used to
+                       connect to para_server.
                [/help]
        [option user-list]
                summary = file which contains user names and credentials
@@ -144,6 +171,18 @@ version-string = GIT_VERSION()
        [option http]
                summary = Options for the http sender
                flag ignored
+       [option http-listen-address]
+               summary = listening addresses of the http sender
+               arg_info = required_arg
+               arg_type = string
+               typestr = addr
+               flag multiple
+               [help]
+                       The http sender of para_server listens on this port for incoming data
+                       connections. This option controls on which addresses the http sender
+                       should listen. See the documentation of the --listen-address above
+                       for the format of the address argument and the defaults.
+               [/help]
        [option http-port]
                summary = TCP port for http streaming
                arg_info = required_arg
@@ -151,9 +190,9 @@ version-string = GIT_VERSION()
                typestr = portnumber
                default_val = 8000
                [help]
-                       The http sender of para_server listens on this port for incoming
-                       connections. Clients are expected to send the usual http request
-                       message such as 'GET / HTTP/'.
+                       This option has the same meaning as --port, but applies to http
+                       data connections and applies to the addresses specified as arguments
+                       to --http-listen-address.
                [/help]
        [option http-default-deny]
                summary = make the http access control list a whitelist
@@ -202,6 +241,15 @@ version-string = GIT_VERSION()
        [option dccp]
                summary = Options for the dccp sender
                flag ignored
+       [option dccp-listen-address]
+               summary = listening addresses of the dccp sender
+               arg_info = required_arg
+               arg_type = string
+               typestr = addr
+               flag multiple
+               [help]
+                       Like --http-listen-address, but for the dccp sender.
+               [/help]
        [option dccp-port]
                summary = port for dccp streaming
                arg_info = required_arg
diff --git a/net.c b/net.c
index 1fece043586cddc782791d0ec18af45881797c5f..ba19408e11337a82aaa380d2e8fe96ceca0c5673 100644 (file)
--- a/net.c
+++ b/net.c
@@ -180,6 +180,36 @@ failed:
        return NULL;
 }
 
+/**
+ * Pretty-print a host/port pair.
+ *
+ * \param url NULL, or any string accepted by \ref parse_url().
+ * \param default_port Applies if url has no port.
+ *
+ * If the url argument is NULL, the function returns the string
+ * 0.0.0.0:default_port. Otherwise it calls \ref parse_url() to check the
+ * syntax of the input string given by url. On errors the string "?" is
+ * returned. Otherwise, if url contains a port, a copy of url is returned. If
+ * no port was supplied, a colon and the default port are appended to url.
+ *
+ * \return In all cases the returned string is a allocated with malloc(3) and
+ * has to be freed by the caller.
+ */
+char *format_url(const char *url, int default_port)
+{
+       char host[MAX_HOSTLEN];
+       int url_port;
+
+       if (!url)
+               return make_message("0.0.0.0:%d", default_port);
+       if (!parse_url(url, host, sizeof(host), &url_port))
+               return make_message("?");
+       if (url_port < 0)
+               return make_message("%s:%d", url, default_port);
+       else
+               return para_strdup(url);
+}
+
 /**
  * Stringify port number, resolve into service name where defined.
  *
@@ -502,17 +532,28 @@ int makesock(unsigned l4type, bool passive, const char *host, uint16_t port_numb
  * Create a passive / listening socket.
  *
  * \param l4type The transport-layer type (\p IPPROTO_xxx).
- * \param port The decimal port number to listen on.
+ * \param addr Passed to \ref parse_url() if not NULL.
+ * \param port Ignored if addr contains a port number.
  *
  * \return Positive integer (socket descriptor) on success, negative value
  * otherwise.
  *
  * \sa \ref makesock(), ip(7), ipv6(7), bind(2), listen(2).
  */
-int para_listen_simple(unsigned l4type, uint16_t port)
+int para_listen(unsigned l4type, const char *addr, uint16_t port)
 {
-       int ret, fd = makesock(l4type, 1, NULL, port, NULL);
-
+       char host[MAX_HOSTLEN];
+       int ret, fd, addr_port;
+
+       if (addr) {
+               if (!parse_url(addr, host, sizeof(host), &addr_port))
+                       return -ERRNO_TO_PARA_ERROR(EINVAL);
+               if (addr_port > 0)
+                       port = addr_port;
+               addr = host;
+       }
+       fd = makesock(l4type, true /* passive */, addr, port,
+               NULL /* no flowopts */);
        if (fd > 0) {
                ret = listen(fd, BACKLOG);
                if (ret < 0) {
@@ -526,6 +567,22 @@ int para_listen_simple(unsigned l4type, uint16_t port)
        return fd;
 }
 
+/**
+ * Create a socket which listens on all network addresses.
+ *
+ * \param l4type See \ref para_listen().
+ * \param port See \ref para_listen().
+ *
+ * This is a simple wrapper for \ref para_listen() which passes a NULL pointer
+ * as the address information.
+ *
+ * \return See \ref para_listen().
+ */
+int para_listen_simple(unsigned l4type, uint16_t port)
+{
+       return para_listen(l4type, NULL, port);
+}
+
 /**
  * Determine IPv4/v6 socket address length.
  * \param sa Container of IPv4 or IPv6 address.
diff --git a/net.h b/net.h
index 143fb812f4c1be20368b93cbb60be26eaeb826fe..2256f376497b89d3382e2bf80ce09725ef64ab02 100644 (file)
--- a/net.h
+++ b/net.h
@@ -71,6 +71,7 @@ extern char *parse_cidr(const char *cidr,
                        char *addr, ssize_t addrlen, int32_t *netmask);
 extern char *parse_url(const char *url,
                       char *host, ssize_t hostlen, int32_t *port);
+char *format_url(const char *url, int default_port);
 extern const char *stringify_port(int port, const char *transport);
 /**
  * Ensure that string conforms to the IPv4 address format.
@@ -128,6 +129,7 @@ bool sockaddr_equal(const struct sockaddr *sa1, const struct sockaddr *sa2);
 /** How many pending connections queue of a listening server will hold. */
 #define BACKLOG        10
 
+int para_listen(unsigned l4type, const char *addr, uint16_t port);
 int para_listen_simple(unsigned l4type, uint16_t port);
 
 /** Pretty-printing of IPv4/6 socket addresses */
diff --git a/send.h b/send.h
index 7a4c01bcb6cc1c80c77e06bc14b164dce8b3304d..67b47e48d9e7fd0b96fa4c5c7abd95594c6e2cf0 100644 (file)
--- a/send.h
+++ b/send.h
@@ -164,10 +164,14 @@ struct fec_client_parms {
 
 /** Describes the current status of one paraslash sender. */
 struct sender_status {
-       /** The file descriptor of the socket this sender is listening on. */
-       int listen_fd;
-       /** The TCP/DCCP port used by this sender. */
-       int port;
+       /** Number of sockets to listen on, size of the two arrays below. */
+       unsigned num_listen_fds;
+       /** Derived from --http-listen-address and --dccp-listen-address. */
+       char **listen_addresses;
+       /** Default TCP/DCCP port number for addresses w/o port. */
+       int default_port;
+       /** The socket fd(s) this sender is listening on. */
+       int *listen_fds;
        /** The current number of simultaneous connections. */
        int num_clients;
        /** The maximal number of simultaneous connections. */
@@ -180,11 +184,15 @@ struct sender_status {
        struct list_head client_list;
 };
 
+/** Iterate over all listening addresses of the http/dccp sender. */
+#define FOR_EACH_LISTEN_FD(_n, _ss) for (_n = 0; _n < (_ss)->num_listen_fds; _n++)
+
 void shutdown_client(struct sender_client *sc, struct sender_status *ss);
 void shutdown_clients(struct sender_status *ss);
 void init_sender_status(struct sender_status *ss,
-               const struct lls_opt_result *acl_opt_result, int port,
-               int max_clients, int default_deny);
+               const struct lls_opt_result *acl_opt_result,
+               const struct lls_opt_result *listen_address_opt_result,
+               int default_port, int max_clients, int default_deny);
 char *generic_sender_status(struct sender_status *ss, const char *name);
 void generic_com_allow(struct sender_command_data *scd,
                struct sender_status *ss);
index 61a12c827342758fa97949db669872e8b2d504b8..24b14ab8ed50b90c956fc6578199f8a4da3f2c4e 100644 (file)
@@ -105,20 +105,38 @@ int send_queued_chunks(int fd, struct chunk_queue *cq)
  *
  * \param ss The struct to initialize.
  * \param acl_opt_result Contains array of --{http|dccp}-access arguments.
- * \param port The tcp or dccp port to listen on.
+ * \param listen_address_opt_result Where to listen on.
+ * \param default_port Used for addresses with no specified port.
  * \param max_clients The maximal number of simultaneous connections.
  * \param default_deny Whether a blacklist should be used for access control.
  */
 void init_sender_status(struct sender_status *ss,
-               const struct lls_opt_result *acl_opt_result, int port,
-               int max_clients, int default_deny)
+               const struct lls_opt_result *acl_opt_result,
+               const struct lls_opt_result *listen_address_opt_result,
+               int default_port, int max_clients, int default_deny)
 {
        int i;
+       unsigned n = lls_opt_given(listen_address_opt_result);
+
+       if (n == 0) {
+               ss->num_listen_fds = 1;
+               ss->listen_addresses = para_malloc(sizeof(char *));
+               ss->listen_addresses[0] = NULL;
+               ss->listen_fds = para_malloc(sizeof(int));
+               ss->listen_fds[0] = -1;
+       } else {
+               ss->num_listen_fds = n;
+               ss->listen_addresses = para_malloc(n * sizeof(char *));
+               ss->listen_fds = para_malloc(n * sizeof(int));
+               FOR_EACH_LISTEN_FD(i, ss) {
+                       ss->listen_addresses[i] = para_strdup(lls_string_val(i,
+                               listen_address_opt_result));
+                       ss->listen_fds[i] = -1;
+               }
+       }
+       ss->default_port = default_port;
 
-       ss->listen_fd = -1;
        INIT_LIST_HEAD(&ss->client_list);
-       ss->port = port;
-
        /* Initialize an access control list */
        INIT_LIST_HEAD(&ss->acl);
        for (i = 0; i < lls_opt_given(acl_opt_result); i++) {
@@ -146,24 +164,35 @@ void init_sender_status(struct sender_status *ss,
  */
 char *generic_sender_status(struct sender_status *ss, const char *name)
 {
-       char *clnts = NULL, *ret;
+       char *clnts = NULL, *ret, *addr = NULL;
        struct sender_client *sc, *tmp_sc;
-
+       unsigned n;
        char *acl_contents = acl_get_contents(&ss->acl);
+
        list_for_each_entry_safe(sc, tmp_sc, &ss->client_list, node) {
                char *tmp = make_message("%s%s ", clnts? clnts : "", sc->name);
                free(clnts);
                clnts = tmp;
        }
+       FOR_EACH_LISTEN_FD(n, ss) {
+               char *url = format_url(ss->listen_addresses[n], ss->default_port);
+               char *tmp = make_message("%s%s%s (fd %d)", addr?
+                       addr : "", addr? ", " : "", url,
+                       ss->listen_fds[n]);
+               free(url);
+               free(addr);
+               addr = tmp;
+       }
        ret = make_message(
-               "status: %s\n"
-               "port: %s\n"
+               "listening address(es): %s\n"
+               "default port: %s\n"
                "number of connected clients: %d\n"
                "maximal number of clients: %d%s\n"
                "connected clients: %s\n"
                "access %s list: %s\n",
-               (ss->listen_fd >= 0)? "on" : "off",
-               stringify_port(ss->port, strcmp(name, "http") ? "dccp" : "tcp"),
+               addr,
+               stringify_port(ss->default_port,
+                       strcmp(name, "http")? "dccp" : "tcp"),
                ss->num_clients,
                ss->max_clients,
                ss->max_clients > 0? "" : " (unlimited)",
@@ -233,29 +262,39 @@ void generic_com_deny(struct sender_command_data *scd,
  */
 void generic_com_on(struct sender_status *ss, unsigned protocol)
 {
-       int fd, ret;
-
-       if (ss->listen_fd >= 0)
-               return;
-       ret = para_listen_simple(protocol, ss->port);
-       if (ret < 0) {
-               PARA_ERROR_LOG("could not listen on port %d: %s\n", ss->port,
-                       para_strerror(-ret));
-               return;
-       }
-       fd = ret;
-       ret = mark_fd_nonblocking(fd);
-       if (ret < 0) {
-               PARA_ERROR_LOG("could not set %s socket fd for port %d to "
-                       "nonblocking mode: %s\n",
-                       protocol == IPPROTO_TCP? "TCP" : "DCCP", ss->port,
-                       para_strerror(-ret));
-               close(fd);
-               return;
+       int ret;
+       unsigned n;
+
+       FOR_EACH_LISTEN_FD(n, ss) {
+               if (ss->listen_fds[n] >= 0)
+                       continue;
+               ret = para_listen(protocol, ss->listen_addresses[n],
+                       ss->default_port);
+               if (ret < 0) {
+                       char *url = format_url(ss->listen_addresses[n],
+                               ss->default_port);
+                       PARA_ERROR_LOG("could not listen on %s %s: %s\n",
+                               protocol == IPPROTO_TCP? "TCP" : "DCCP",
+                               url, para_strerror(-ret));
+                       free(url);
+                       continue;
+               }
+               ss->listen_fds[n] = ret;
+               ret = mark_fd_nonblocking(ss->listen_fds[n]);
+               if (ret < 0) {
+                       char *url = format_url(ss->listen_addresses[n],
+                               ss->default_port);
+                       PARA_ERROR_LOG("could not set %s socket fd for %s to "
+                               "nonblocking mode: %s\n",
+                               protocol == IPPROTO_TCP? "TCP" : "DCCP", url,
+                               para_strerror(-ret));
+                       free(url);
+                       close(ss->listen_fds[n]);
+                       ss->listen_fds[n] = -1;
+                       continue;
+               }
+               add_close_on_fork_list(ss->listen_fds[n]);
        }
-       add_close_on_fork_list(fd);
-       ss->listen_fd = fd;
-       return;
 }
 
 /**
@@ -269,23 +308,26 @@ void generic_com_on(struct sender_status *ss, unsigned protocol)
  */
 void generic_com_off(struct sender_status *ss)
 {
-       if (ss->listen_fd < 0)
-               return;
-       PARA_NOTICE_LOG("closing port %d\n", ss->port);
-       close(ss->listen_fd);
-       del_close_on_fork_list(ss->listen_fd);
-       shutdown_clients(ss);
-       ss->listen_fd = -1;
+       unsigned n;
+
+       FOR_EACH_LISTEN_FD(n, ss) {
+               if (ss->listen_fds[n] < 0)
+                       return;
+               close(ss->listen_fds[n]);
+               del_close_on_fork_list(ss->listen_fds[n]);
+               shutdown_clients(ss);
+               ss->listen_fds[n] = -1;
+       }
 }
 
 /**
- * Accept a connection on the socket this server is listening on.
+ * Accept a connection on the socket(s) this server is listening on.
  *
  * \param ss The sender whose listening fd is ready for reading.
  * \param rfds Passed to para_accept(),
  *
- * This calls para_accept() and performs the following actions on the resulting
- * file descriptor fd:
+ * This accepts incoming connections on any of the listening sockets of the
+ * server. If there is a connection pending, the function
  *
  *     - Checks whether the maximal number of connections are exceeded.
  *     - Sets \a fd to nonblocking mode.
@@ -310,36 +352,40 @@ struct sender_client *accept_sender_client(struct sender_status *ss, fd_set *rfd
 {
        struct sender_client *sc;
        int fd, ret;
+       unsigned n;
 
-       if (ss->listen_fd < 0)
-               return NULL;
-       ret = para_accept(ss->listen_fd, rfds, NULL, 0, &fd);
-       if (ret < 0)
-               PARA_ERROR_LOG("%s\n", para_strerror(-ret));
-       if (ret <= 0)
-               return NULL;
-       ret = -E_MAX_CLIENTS;
-       if (ss->max_clients > 0 && ss->num_clients >= ss->max_clients)
-               goto err_out;
-       ret = mark_fd_nonblocking(fd);
-       if (ret < 0)
-               goto err_out;
-       ret = acl_check_access(fd, &ss->acl, ss->default_deny);
-       if (ret < 0)
-               goto err_out;
-       ss->num_clients++;
-       sc = para_calloc(sizeof(*sc));
-       sc->fd = fd;
-       sc->name = para_strdup(remote_name(fd));
-       sc->cq = cq_new(MAX_CQ_BYTES);
-       para_list_add(&sc->node, &ss->client_list);
-       add_close_on_fork_list(fd);
-       PARA_INFO_LOG("accepted client #%d: %s (fd %d)\n", ss->num_clients,
-               sc->name, fd);
-       return sc;
-err_out:
-       PARA_WARNING_LOG("%s\n", para_strerror(-ret));
-       close(fd);
+       FOR_EACH_LISTEN_FD(n, ss) {
+               if (ss->listen_fds[n] < 0)
+                       continue;
+               ret = para_accept(ss->listen_fds[n], rfds, NULL, 0, &fd);
+               if (ret < 0)
+                       goto warn;
+               if (ret == 0)
+                       continue;
+               ret = -E_MAX_CLIENTS;
+               if (ss->max_clients > 0 && ss->num_clients >= ss->max_clients)
+                       goto close_fd_and_warn;
+               ret = mark_fd_nonblocking(fd);
+               if (ret < 0)
+                       goto close_fd_and_warn;
+               ret = acl_check_access(fd, &ss->acl, ss->default_deny);
+               if (ret < 0)
+                       goto close_fd_and_warn;
+               ss->num_clients++;
+               sc = para_calloc(sizeof(*sc));
+               sc->fd = fd;
+               sc->name = para_strdup(remote_name(fd));
+               sc->cq = cq_new(MAX_CQ_BYTES);
+               para_list_add(&sc->node, &ss->client_list);
+               add_close_on_fork_list(fd);
+               PARA_INFO_LOG("accepted client #%d: %s (fd %d)\n", ss->num_clients,
+                       sc->name, fd);
+               return sc;
+close_fd_and_warn:
+               close(fd);
+warn:
+               PARA_WARNING_LOG("%s\n", para_strerror(-ret));
+       }
        return NULL;
 }
 
index f19fc996ecc7355ddf24379e242fcec4da287dd6..a344b77440751a6e7017d9d7e641390a36eb157f 100644 (file)
--- a/server.c
+++ b/server.c
@@ -129,8 +129,9 @@ bool process_is_command_handler(void)
 
 /** The task responsible for server command handling. */
 struct server_command_task {
-       /** TCP port on which para_server listens for connections. */
-       int listen_fd;
+       unsigned num_listen_fds; /* only one by default */
+       /** TCP socket(s) on which para_server listens for connections. */
+       int *listen_fds;
        /* File descriptor for the accepted socket. */
        int child_fd;
        /** Copied from para_server's main function. */
@@ -382,22 +383,22 @@ static void init_signal_task(void)
 
 static void command_pre_select(struct sched *s, void *context)
 {
+       unsigned n;
        struct server_command_task *sct = context;
-       para_fd_set(sct->listen_fd, &s->rfds, &s->max_fileno);
+
+       for (n = 0; n < sct->num_listen_fds; n++)
+               para_fd_set(sct->listen_fds[n], &s->rfds, &s->max_fileno);
 }
 
-static int command_post_select(struct sched *s, void *context)
+static int command_task_accept(unsigned listen_idx, struct sched *s,
+               struct server_command_task *sct)
 {
-       struct server_command_task *sct = context;
        int new_fd, ret, i;
        char *peer_name;
        pid_t child_pid;
        uint32_t *chunk_table;
 
-       ret = task_get_notification(sct->task);
-       if (ret < 0)
-               return ret;
-       ret = para_accept(sct->listen_fd, &s->rfds, NULL, 0, &new_fd);
+       ret = para_accept(sct->listen_fds[listen_idx], &s->rfds, NULL, 0, &new_fd);
        if (ret <= 0)
                goto out;
        mmd->num_connects++;
@@ -459,23 +460,63 @@ out:
        return 0;
 }
 
+static int command_post_select(struct sched *s, void *context)
+{
+       struct server_command_task *sct = context;
+       unsigned n;
+       int ret;
+
+       ret = task_get_notification(sct->task);
+       if (ret < 0)
+               return ret;
+       for (n = 0; n < sct->num_listen_fds; n++) {
+               ret = command_task_accept(n, s, sct);
+               if (ret < 0) {
+                       free(sct->listen_fds);
+                       return ret;
+               }
+       }
+       return 0;
+}
+
 static void init_server_command_task(struct server_command_task *sct,
                int argc, char **argv)
 {
        int ret;
+       unsigned n;
+       uint32_t port = OPT_UINT32_VAL(PORT);
 
        PARA_NOTICE_LOG("initializing tcp command socket\n");
        sct->child_fd = -1;
        sct->argc = argc;
        sct->argv = argv;
-       ret = para_listen_simple(IPPROTO_TCP, OPT_UINT32_VAL(PORT));
-       if (ret < 0)
-               goto err;
-       sct->listen_fd = ret;
-       ret = mark_fd_nonblocking(sct->listen_fd);
-       if (ret < 0)
-               goto err;
-       add_close_on_fork_list(sct->listen_fd); /* child doesn't need the listener */
+       if (!OPT_GIVEN(LISTEN_ADDRESS)) {
+               sct->num_listen_fds = 1;
+               sct->listen_fds = para_malloc(sizeof(int));
+               ret = para_listen_simple(IPPROTO_TCP, port);
+               if (ret < 0)
+                       goto err;
+               sct->listen_fds[0] = ret;
+       } else {
+               sct->num_listen_fds = OPT_GIVEN(LISTEN_ADDRESS);
+               sct->listen_fds = para_malloc(sct->num_listen_fds * sizeof(int));
+               for (n = 0; n < OPT_GIVEN(LISTEN_ADDRESS); n++) {
+                       const char *arg;
+                       arg = lls_string_val(n, OPT_RESULT(LISTEN_ADDRESS));
+                       ret = para_listen(IPPROTO_TCP, arg, port);
+                       if (ret < 0)
+                               goto err;
+                       sct->listen_fds[n] = ret;
+               }
+       }
+       for (n = 0; n < sct->num_listen_fds; n++) {
+               ret = mark_fd_nonblocking(sct->listen_fds[n]);
+               if (ret < 0)
+                       goto err;
+               /* child doesn't need the listener */
+               add_close_on_fork_list(sct->listen_fds[n]);
+       }
+
        sct->task = task_register(&(struct task_info) {
                .name = "server command",
                .pre_select = command_pre_select,