Add Nicira extension for remote command execution.
authorBen Pfaff <blp@nicira.com>
Mon, 27 Oct 2008 19:55:55 +0000 (12:55 -0700)
committerBen Pfaff <blp@nicira.com>
Mon, 27 Oct 2008 19:58:27 +0000 (12:58 -0700)
22 files changed:
Makefile.am
configure.ac
debian/openflow-switch.default
debian/openflow-switch.init
debian/openflow-switch.install
include/dirs.h
include/nicira-ext.h
include/socket-util.h
include/vlog-modules.def
lib/automake.mk
lib/socket-util.c
secchan/automake.mk
secchan/commands/automake.mk [new file with mode: 0644]
secchan/commands/reboot [new file with mode: 0755]
secchan/commands/update [new file with mode: 0755]
secchan/executer.c [new file with mode: 0644]
secchan/executer.h [new file with mode: 0644]
secchan/secchan.8.in
secchan/secchan.c
secchan/secchan.h
utilities/dpctl.8.in
utilities/dpctl.c

index 8634a21..889445a 100644 (file)
@@ -34,6 +34,7 @@ TESTS =
 TESTS_ENVIRONMENT =
 bin_PROGRAMS =
 bin_SCRIPTS =
 TESTS_ENVIRONMENT =
 bin_PROGRAMS =
 bin_SCRIPTS =
+dist_commands_DATA =
 dist_man_MANS =
 man_MANS =
 noinst_HEADERS =
 dist_man_MANS =
 man_MANS =
 noinst_HEADERS =
@@ -46,6 +47,7 @@ EXTRA_DIST += README.hwtables
 do_subst = ($(srcdir)/subst VLOG_OPTIONS $(srcdir)/lib/vlog.man | \
            sed -e 's,[@]PKIDIR[@],$(PKIDIR),g' \
                 -e 's,[@]RUNDIR[@],$(RUNDIR),g' \
 do_subst = ($(srcdir)/subst VLOG_OPTIONS $(srcdir)/lib/vlog.man | \
            sed -e 's,[@]PKIDIR[@],$(PKIDIR),g' \
                 -e 's,[@]RUNDIR[@],$(RUNDIR),g' \
+                -e 's,[@]pkgdatadir[@],$(pkgdatadir),g' \
                 -e 's,[@]PERL[@],$(PERL),g')
 ro_script = sed "`printf '1a\\' && printf '\\n\# -*- buffer-read-only: t -*-'`"
 ro_c = echo '/* -*- mode: c; buffer-read-only: t -*- */'
                 -e 's,[@]PERL[@],$(PERL),g')
 ro_script = sed "`printf '1a\\' && printf '\\n\# -*- buffer-read-only: t -*-'`"
 ro_c = echo '/* -*- mode: c; buffer-read-only: t -*- */'
index f4e923a..4501bb8 100644 (file)
@@ -53,6 +53,8 @@ OFP_CHECK_LIBOPENFLOW
 OFP_CHECK_IF_PACKET
 OFP_CHECK_HWTABLES
 
 OFP_CHECK_IF_PACKET
 OFP_CHECK_HWTABLES
 
+AC_CHECK_FUNCS([strsignal])
+
 AC_ARG_VAR(KARCH, [Kernel Architecture String])
 AC_SUBST(KARCH)
 OFP_CHECK_LINUX(l26, 2.6, 2.6, KSRC26, L26_ENABLED)
 AC_ARG_VAR(KARCH, [Kernel Architecture String])
 AC_SUBST(KARCH)
 OFP_CHECK_LINUX(l26, 2.6, 2.6, KSRC26, L26_ENABLED)
index ec3696a..5112332 100644 (file)
@@ -99,5 +99,12 @@ SWITCH_IP=dhcp
 # use openflow-switchmon.
 MGMT_VCONNS="punix:/var/run/secchan.socket"
 
 # use openflow-switchmon.
 MGMT_VCONNS="punix:/var/run/secchan.socket"
 
+# COMMANDS: Access control list for the commands that can be executed
+# remotely over the OpenFlow protocol, as a comma-separated list of
+# shell glob patterns.  Negative patterns (beginning with !) act as a
+# blacklist.  To be executable, a command name must match one positive
+# pattern and not match any negative patterns.
+#COMMANDS="reboot,update"
+
 # DAEMON_OPTS: Additional options to pass to secchan, e.g. "--fail=open"
 DAEMON_OPTS=""
 # DAEMON_OPTS: Additional options to pass to secchan, e.g. "--fail=open"
 DAEMON_OPTS=""
index c3d22d7..7945799 100755 (executable)
@@ -34,6 +34,17 @@ DODTIME=1                   # Time to wait for the server to die, in seconds
                             # 'restart' will not work
 
 # Include secchan defaults if available
                             # 'restart' will not work
 
 # Include secchan defaults if available
+unset NETDEVS
+unset MODE
+unset SWITCH_IP
+unset CONTROLLER
+unset PRIVKEY
+unset CERT
+unset CACERT
+unset CACERT_MODE
+unset MGMT_VCONNS
+unset COMMANDS
+unset DAEMON_OPTS
 default=/etc/default/openflow-switch
 if [ -f $default ] ; then
        . $default
 default=/etc/default/openflow-switch
 if [ -f $default ] ; then
        . $default
@@ -223,10 +234,16 @@ case "$1" in
             MGMT_OPTS="$MGMT_OPTS --listen=$vconn"
         done
 
             MGMT_OPTS="$MGMT_OPTS --listen=$vconn"
         done
 
+        COMMAND_OPT=
+        if test -n "$COMMANDS"; then
+            COMMAND_OPT="--commands=$COMMANDS"
+        fi
+
        echo -n "Starting $DESC: "
        start-stop-daemon --start --quiet --pidfile $PIDFILE \
            --exec $DAEMON -- nl:0 $CONTROLLER --detach --pidfile=$PIDFILE \
        echo -n "Starting $DESC: "
        start-stop-daemon --start --quiet --pidfile $PIDFILE \
            --exec $DAEMON -- nl:0 $CONTROLLER --detach --pidfile=$PIDFILE \
-            --verbose=ANY:console:emer $DAEMON_OPTS $MGMT_OPTS $SSL_OPTS
+            --verbose=ANY:console:emer $DAEMON_OPTS $MGMT_OPTS $SSL_OPTS \
+            "$COMMAND_OPT"
         if running; then
             echo "$NAME."
         else
         if running; then
             echo "$NAME."
         else
index 2c85136..3d2e9e9 100644 (file)
@@ -4,3 +4,4 @@ _debian/utilities/dpctl usr/sbin
 _debian/utilities/ofp-discover usr/sbin
 _debian/utilities/ofp-kill usr/sbin
 debian/ofp-switch-setup usr/sbin
 _debian/utilities/ofp-discover usr/sbin
 _debian/utilities/ofp-kill usr/sbin
 debian/ofp-switch-setup usr/sbin
+debian/openflow/usr/share/openflow/commands usr/share/openflow/commands
index 36048ce..f5de08a 100644 (file)
@@ -34,7 +34,8 @@
 #ifndef DIRS_H
 #define DIRS_H 1
 
 #ifndef DIRS_H
 #define DIRS_H 1
 
-extern const char ofp_rundir[];  /* /usr/local/var/run */
-extern const char ofp_logdir[];  /* /usr/local/var/log */
+extern const char ofp_pkgdatadir[]; /* /usr/local/share/openflow */
+extern const char ofp_rundir[];     /* /usr/local/var/run */
+extern const char ofp_logdir[];     /* /usr/local/var/log */
 
 #endif /* dirs.h */
 
 #endif /* dirs.h */
index 4c5604e..9a7a3d0 100644 (file)
@@ -29,7 +29,16 @@ enum nicira_type {
     NXT_ACT_SET_CONFIG,
 
     /* Get configuration of action. */
     NXT_ACT_SET_CONFIG,
 
     /* Get configuration of action. */
-    NXT_ACT_GET_CONFIG
+    NXT_ACT_GET_CONFIG,
+
+    /* Remote command execution.  The request body is a sequence of strings
+     * delimited by null bytes.  The first string is a command name.
+     * Subsequent strings are command arguments. */
+    NXT_COMMAND_REQUEST,
+
+    /* Remote command execution reply, sent when the command's execution
+     * completes.  The reply body is struct nx_command_reply. */
+    NXT_COMMAND_REPLY
 };
 
 struct nicira_header {
 };
 
 struct nicira_header {
@@ -107,4 +116,22 @@ struct nx_action_header {
 };
 OFP_ASSERT(sizeof(struct nx_action_header) == 16);
 
 };
 OFP_ASSERT(sizeof(struct nx_action_header) == 16);
 
+/* Status bits for NXT_COMMAND_REPLY. */
+enum {
+    NXT_STATUS_EXITED = 0x8000,   /* Exited normally. */
+    NXT_STATUS_SIGNALED = 0x4000, /* Exited due to signal. */
+    NXT_STATUS_UNKNOWN = 0x2000,  /* Exited for unknown reason. */
+    NXT_STATUS_COREDUMP = 0x1000, /* Exited with core dump. */
+    NXT_STATUS_EXITSTATUS = 0xff, /* Exit code mask if NXT_STATUS_EXITED. */
+    NXT_STATUS_TERMSIG = 0xff,    /* Signal number if NXT_STATUS_SIGNALED. */
+};
+
+/* NXT_COMMAND_REPLY. */
+struct nx_command_reply {
+    struct nicira_header nxh;
+    uint32_t status;            /* Status bits defined above. */
+    /* Followed by any number of bytes of process output. */
+};
+OFP_ASSERT(sizeof(struct nx_command_reply) == 20);
+
 #endif /* nicira-ext.h */
 #endif /* nicira-ext.h */
index 4ae1068..3816c3c 100644 (file)
@@ -39,6 +39,7 @@
 #include <stdbool.h>
 
 int set_nonblocking(int fd);
 #include <stdbool.h>
 
 int set_nonblocking(int fd);
+int get_max_fds(void);
 int lookup_ip(const char *host_name, struct in_addr *address);
 int get_socket_error(int sock);
 int check_connection_completion(int fd);
 int lookup_ip(const char *host_name, struct in_addr *address);
 int get_socket_error(int sock);
 int check_connection_completion(int fd);
index 9df65e1..2f6a676 100644 (file)
@@ -9,6 +9,7 @@ VLOG_MODULE(dhcp_client)
 VLOG_MODULE(discovery)
 VLOG_MODULE(dpif)
 VLOG_MODULE(dpctl)
 VLOG_MODULE(discovery)
 VLOG_MODULE(dpif)
 VLOG_MODULE(dpctl)
+VLOG_MODULE(executer)
 VLOG_MODULE(fail_open)
 VLOG_MODULE(fault)
 VLOG_MODULE(flow)
 VLOG_MODULE(fail_open)
 VLOG_MODULE(fault)
 VLOG_MODULE(flow)
index 7b4da91..38f58e5 100644 (file)
@@ -63,6 +63,7 @@ EXTRA_DIST += \
 CLEANFILES += lib/dirs.c
 lib/dirs.c: Makefile
        ($(ro_c) && \
 CLEANFILES += lib/dirs.c
 lib/dirs.c: Makefile
        ($(ro_c) && \
+        echo 'const char ofp_pkgdatadir[] = "$(pkgdatadir$)";' && \
         echo 'const char ofp_rundir[] = "@RUNDIR@";' && \
         echo 'const char ofp_logdir[] = "@LOGDIR@";') > lib/dirs.c.tmp
        mv lib/dirs.c.tmp lib/dirs.c
         echo 'const char ofp_rundir[] = "@RUNDIR@";' && \
         echo 'const char ofp_logdir[] = "@LOGDIR@";') > lib/dirs.c.tmp
        mv lib/dirs.c.tmp lib/dirs.c
index 781bcb8..d8ae179 100644 (file)
@@ -41,6 +41,7 @@
 #include <stddef.h>
 #include <stdio.h>
 #include <string.h>
 #include <stddef.h>
 #include <stdio.h>
 #include <string.h>
+#include <sys/resource.h>
 #include <sys/un.h>
 #include <unistd.h>
 #include "fatal-signal.h"
 #include <sys/un.h>
 #include <unistd.h>
 #include "fatal-signal.h"
@@ -68,6 +69,26 @@ set_nonblocking(int fd)
     }
 }
 
     }
 }
 
+/* Returns the maximum valid FD value, plus 1. */
+int
+get_max_fds(void)
+{
+    static int max_fds = -1;
+    if (max_fds < 0) {
+        struct rlimit r;
+        if (!getrlimit(RLIMIT_NOFILE, &r)
+            && r.rlim_cur != RLIM_INFINITY
+            && r.rlim_cur != RLIM_SAVED_MAX
+            && r.rlim_cur != RLIM_SAVED_CUR) {
+            max_fds = r.rlim_cur;
+        } else {
+            VLOG_WARN("failed to obtain fd limit, defaulting to 1024");
+            max_fds = 1024;
+        }
+    }
+    return max_fds;
+}
+
 /* Translates 'host_name', which may be a DNS name or an IP address, into a
  * numeric IP address in '*addr'.  Returns 0 if successful, otherwise a
  * positive errno value. */
 /* Translates 'host_name', which may be a DNS name or an IP address, into a
  * numeric IP address in '*addr'.  Returns 0 if successful, otherwise a
  * positive errno value. */
index 4572e8a..d5472f0 100644 (file)
@@ -4,6 +4,8 @@ man_MANS += secchan/secchan.8
 secchan_secchan_SOURCES = \
        secchan/discovery.c \
        secchan/discovery.h \
 secchan_secchan_SOURCES = \
        secchan/discovery.c \
        secchan/discovery.h \
+       secchan/executer.c \
+       secchan/executer.h \
        secchan/fail-open.c \
        secchan/fail-open.h \
        secchan/in-band.c \
        secchan/fail-open.c \
        secchan/fail-open.h \
        secchan/in-band.c \
@@ -13,6 +15,7 @@ secchan_secchan_SOURCES = \
        secchan/ratelimit.c \
        secchan/ratelimit.h \
        secchan/secchan.c \
        secchan/ratelimit.c \
        secchan/ratelimit.h \
        secchan/secchan.c \
+       secchan/secchan.h \
        secchan/status.c \
        secchan/status.h \
        secchan/stp-secchan.c \
        secchan/status.c \
        secchan/status.h \
        secchan/stp-secchan.c \
@@ -26,3 +29,5 @@ secchan_secchan_LDADD = lib/libopenflow.a $(FAULT_LIBS) $(SSL_LIBS)
 
 EXTRA_DIST += secchan/secchan.8.in
 DISTCLEANFILES += secchan/secchan.8
 
 EXTRA_DIST += secchan/secchan.8.in
 DISTCLEANFILES += secchan/secchan.8
+
+include secchan/commands/automake.mk
diff --git a/secchan/commands/automake.mk b/secchan/commands/automake.mk
new file mode 100644 (file)
index 0000000..1b291f8
--- /dev/null
@@ -0,0 +1,4 @@
+commandsdir = ${pkgdatadir}/commands
+dist_commands_SCRIPTS = \
+       secchan/commands/reboot \
+       secchan/commands/update
diff --git a/secchan/commands/reboot b/secchan/commands/reboot
new file mode 100755 (executable)
index 0000000..8fc9314
--- /dev/null
@@ -0,0 +1,2 @@
+#! /bin/sh
+reboot
diff --git a/secchan/commands/update b/secchan/commands/update
new file mode 100755 (executable)
index 0000000..4d617fe
--- /dev/null
@@ -0,0 +1,3 @@
+#! /bin/sh -e
+apt-get update -qy
+apt-get upgrade -qy
diff --git a/secchan/executer.c b/secchan/executer.c
new file mode 100644 (file)
index 0000000..45f69c9
--- /dev/null
@@ -0,0 +1,481 @@
+/* Copyright (c) 2008 The Board of Trustees of The Leland Stanford
+ * Junior University
+ *
+ * We are making the OpenFlow specification and associated documentation
+ * (Software) available for public use and benefit with the expectation
+ * that others will use, modify and enhance the Software and contribute
+ * those enhancements back to the community. However, since we would
+ * like to make the Software available for broadest use, with as few
+ * restrictions as possible permission is hereby granted, free of
+ * charge, to any person obtaining a copy of this Software to deal in
+ * the Software under the copyrights without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT.  IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ * The name and trademarks of copyright holder(s) may NOT be used in
+ * advertising or publicity pertaining to the Software or any
+ * derivatives without specific, written prior permission.
+ */
+
+#include <config.h>
+#include "executer.h"
+#include <errno.h>
+#include <fcntl.h>
+#include <fnmatch.h>
+#include <poll.h>
+#include <signal.h>
+#include <stdlib.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+#include <string.h>
+#include <unistd.h>
+#include "dynamic-string.h"
+#include "fatal-signal.h"
+#include "nicira-ext.h"
+#include "ofpbuf.h"
+#include "openflow.h"
+#include "poll-loop.h"
+#include "rconn.h"
+#include "secchan.h"
+#include "socket-util.h"
+#include "util.h"
+#include "vconn.h"
+
+#define THIS_MODULE VLM_executer
+#include "vlog.h"
+
+#define MAX_CHILDREN 8
+
+struct child {
+    /* Information about child process. */
+    char *name;                 /* argv[0] passed to child. */
+    pid_t pid;                  /* Child's process ID. */
+
+    /* For sending a reply to the controller when the child dies. */
+    struct relay *relay;
+    uint32_t xid;               /* Transaction ID used by controller. */
+
+    /* We read up to MAX_OUTPUT bytes of output and send them back to the
+     * controller when the child dies. */
+#define MAX_OUTPUT 4096
+    int output_fd;              /* FD from which to read child's output. */
+    uint8_t *output;            /* Output data. */
+    size_t output_size;         /* Number of bytes of output data so far. */
+};
+
+struct executer {
+    const struct settings *s;
+
+    /* Children. */
+    struct child children[MAX_CHILDREN];
+    size_t n_children;
+
+    /* File descriptors. */
+    int wait_fd;                /* Pipe FD for wakeup when on SIGCHLD. */
+    int null_fd;                /* FD for /dev/null. */
+};
+
+/* Returns true if 'cmd' is allowed by 'acl', which is a command-separated
+ * access control list in the format described for --command-acl in
+ * secchan(8). */
+static bool
+executer_is_permitted(const char *acl_, const char *cmd)
+{
+    char *acl, *save_ptr, *pattern;
+    bool allowed, denied;
+
+    /* Verify that 'cmd' consists only of alphanumerics plus _ or -. */
+    if (cmd[strspn(cmd, "abcdefghijklmnopqrstuvwxyz"
+                   "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-")] != '\0') {
+        VLOG_WARN("rejecting command name \"%s\" that contain forbidden "
+                  "characters", cmd);
+        return false;
+    }
+
+    /* Check 'cmd' against 'acl'. */
+    acl = xstrdup(acl_);
+    save_ptr = acl;
+    allowed = denied = false;
+    while ((pattern = strsep(&save_ptr, ",")) != NULL && !denied) {
+        if (pattern[0] != '!' && !fnmatch(pattern, cmd, 0)) {
+            allowed = true;
+        } else if (pattern[0] == '!' && !fnmatch(pattern + 1, cmd, 0)) {
+            denied = true;
+        }
+    }
+    free(acl);
+
+    /* Check the command white/blacklisted state. */
+    if (allowed && !denied) {
+        VLOG_WARN("permitting command execution: \"%s\" is whitelisted", cmd);
+    } else if (allowed && denied) {
+        VLOG_WARN("denying command execution: \"%s\" is both blacklisted "
+                  "and whitelisted", cmd);
+    } else if (!allowed) {
+        VLOG_WARN("denying command execution: \"%s\" is not whitelisted", cmd);
+    } else if (denied) {
+        VLOG_WARN("denying command execution: \"%s\" is blacklisted", cmd);
+    }
+    return allowed && !denied;
+}
+
+static bool
+executer_remote_packet_cb(struct relay *r, void *e_)
+{
+    struct executer *e = e_;
+    struct ofpbuf *msg = r->halves[HALF_REMOTE].rxbuf;
+    struct nicira_header *request;
+    char **argv;
+    char *args;
+    char *exec_file = NULL;
+    int max_fds;
+    struct stat s;
+    size_t args_size;
+    size_t argc;
+    size_t i;
+    pid_t pid;
+    int output_fds[2];
+
+    /* Check for NXT_COMMAND_REQUEST vendor extension. */
+    if (msg->size < sizeof(struct nicira_header)) {
+        return false;
+    }
+    request = msg->data;
+    if (request->header.type != OFPT_VENDOR
+        || request->vendor != htonl(NX_VENDOR_ID)
+        || request->subtype != htonl(NXT_COMMAND_REQUEST)) {
+        return false;
+    }
+
+    /* Verify limit on children not exceeded.
+     * XXX should probably kill children when the connection drops? */
+    if (e->n_children >= MAX_CHILDREN) {
+        VLOG_WARN("limit of %d child processes reached, dropping request",
+                  MAX_CHILDREN);
+        return false;
+    }
+
+    /* Copy argument buffer, adding a null terminator at the end.  Now every
+     * argument is null-terminated, instead of being merely null-delimited. */
+    args_size = msg->size - sizeof *request;
+    args = xmemdup0((const void *) (request + 1), args_size);
+
+    /* Count arguments. */
+    argc = 0;
+    for (i = 0; i <= args_size; i++) {
+        argc += args[i] == '\0';
+    }
+
+    /* Set argv[*] to point to each argument. */
+    argv = xmalloc((argc + 1) * sizeof *argv);
+    argv[0] = args;
+    for (i = 1; i < argc; i++) {
+        argv[i] = strchr(argv[i - 1], '\0') + 1;
+    }
+    argv[argc] = NULL;
+
+    /* Check permissions. */
+    if (!executer_is_permitted(e->s->command_acl, argv[0])) {
+        goto done;
+    }
+
+    /* Find the executable. */
+    exec_file = xasprintf("%s/%s", e->s->command_dir, argv[0]);
+    if (stat(exec_file, &s)) {
+        VLOG_WARN("failed to stat \"%s\": %s", exec_file, strerror(errno));
+        goto done;
+    }
+    if (!S_ISREG(s.st_mode)) {
+        VLOG_WARN("\"%s\" is not a regular file", exec_file);
+        goto done;
+    }
+    argv[0] = exec_file;
+
+    /* Arrange to capture output. */
+    if (pipe(output_fds)) {
+        VLOG_WARN("pipe failed: %s", strerror(errno));
+        goto done;
+    }
+
+    pid = fork();
+    if (!pid) {
+        /* Running in child.
+         * XXX should run in new process group so that we can signal all
+         * subprocesses at once?  Would also want to catch fatal signals and
+         * kill them at the same time though. */
+        fatal_signal_fork();
+        dup2(e->null_fd, 0);
+        dup2(output_fds[1], 1);
+        dup2(e->null_fd, 2);
+        max_fds = get_max_fds();
+        for (i = 3; i < max_fds; i++) {
+            close(i);
+        }
+        if (chdir(e->s->command_dir)) {
+            printf("could not change directory to \"%s\": %s",
+                   e->s->command_dir, strerror(errno));
+            exit(EXIT_FAILURE);
+        }
+        execv(argv[0], argv);
+        printf("failed to start \"%s\": %s\n", argv[0], strerror(errno));
+        exit(EXIT_FAILURE);
+    } else if (pid > 0) {
+        /* Running in parent. */
+        struct child *child;
+
+        VLOG_WARN("started \"%s\" subprocess", argv[0]);
+        child = &e->children[e->n_children++];
+        child->name = xstrdup(argv[0]);
+        child->pid = pid;
+        child->relay = r;
+        child->xid = request->header.xid;
+        child->output_fd = output_fds[0];
+        child->output = xmalloc(MAX_OUTPUT);
+        child->output_size = 0;
+        set_nonblocking(output_fds[0]);
+        close(output_fds[1]);
+    } else {
+        VLOG_WARN("fork failed: %s", strerror(errno));
+        close(output_fds[0]);
+        close(output_fds[1]);
+    }
+
+done:
+    free(exec_file);
+    free(args);
+    free(argv);
+    return true;
+}
+
+/* 'child' died with 'status' as its return code.  Deal with it. */
+static void
+child_terminated(struct child *child, int status)
+{
+    /* Log how it terminated. */
+    struct ds ds = DS_EMPTY_INITIALIZER;
+    if (WIFEXITED(status)) {
+        ds_put_format(&ds, "normally with status %d", WEXITSTATUS(status));
+    } else if (WIFSIGNALED(status)) {
+#ifdef HAVE_STRSIGNAL
+        ds_put_format(&ds, "by signal %s", strsignal(WTERMSIG(status)));
+#else
+        ds_put_format(&ds, "by signal %d", WTERMSIG(status));
+#endif
+    }
+    if (WCOREDUMP(status)) {
+        ds_put_cstr(&ds, " (core dumped)");
+    }
+    VLOG_WARN("child process \"%s\" with pid %ld terminated %s",
+              child->name, (long int) child->pid, ds_cstr(&ds));
+    ds_destroy(&ds);
+
+    /* Send a status message back to the controller that requested the
+     * command. */
+    if (child->relay) {
+        struct nx_command_reply *r;
+        struct ofpbuf *buffer;
+
+        r = make_openflow_xid(sizeof *r, OFPT_VENDOR, child->xid, &buffer);
+        r->nxh.vendor = htonl(NX_VENDOR_ID);
+        r->nxh.subtype = htonl(NXT_COMMAND_REPLY);
+        if (WIFEXITED(status)) {
+            r->status = htonl(WEXITSTATUS(status) | NXT_STATUS_EXITED);
+        } else if (WIFSIGNALED(status)) {
+            r->status = htonl(WTERMSIG(status) | NXT_STATUS_SIGNALED);
+        } else {
+            r->status = htonl(NXT_STATUS_UNKNOWN);
+        }
+        if (WCOREDUMP(status)) {
+            r->status |= htonl(NXT_STATUS_COREDUMP);
+        }
+        ofpbuf_put(buffer, child->output, child->output_size);
+        update_openflow_length(buffer);
+        if (rconn_send(child->relay->halves[HALF_REMOTE].rconn, buffer,
+                       NULL)) {
+            ofpbuf_delete(buffer);
+        }
+    }
+}
+
+/* Read output from 'child' and append it to its output buffer. */
+static void
+poll_child(struct child *child)
+{
+    ssize_t n;
+
+    if (child->output_fd < 0) {
+        return;
+    }
+
+    do {
+        n = read(child->output_fd, child->output + child->output_size,
+                 MAX_OUTPUT - child->output_size);
+    } while (n < 0 && errno == EINTR);
+    if (n > 0) {
+        child->output_size += n;
+        if (child->output_size < MAX_OUTPUT) {
+            return;
+        }
+    } else if (n < 0 && errno == EAGAIN) {
+        return;
+    }
+    close(child->output_fd);
+    child->output_fd = -1;
+}
+
+static void
+executer_periodic_cb(void *e_)
+{
+    struct executer *e = e_;
+    char buffer[MAX_CHILDREN];
+    size_t i;
+
+    if (!e->n_children) {
+        return;
+    }
+
+    /* Read output from children. */
+    for (i = 0; i < e->n_children; i++) {
+        struct child *child = &e->children[i];
+        poll_child(child);
+    }
+
+    /* If SIGCHLD was received, reap dead children. */
+    if (read(e->wait_fd, buffer, sizeof buffer) <= 0) {
+        return;
+    }
+    for (;;) {
+        int status;
+        pid_t pid;
+
+        /* Get dead child in 'pid' and its return code in 'status'. */
+        pid = waitpid(WAIT_ANY, &status, WNOHANG);
+        if (pid < 0 && errno == EINTR) {
+            continue;
+        } else if (pid <= 0) {
+            return;
+        }
+
+        /* Find child with given 'pid' and drop it from the list. */
+        for (i = 0; i < e->n_children; i++) {
+            struct child *child = &e->children[i];
+            if (child->pid == pid) {
+                poll_child(child);
+                child_terminated(child, status);
+                free(child->name);
+                free(child->output);
+                *child = e->children[--e->n_children];
+                goto found;
+            }
+        }
+        VLOG_WARN("child with unknown pid %ld terminated", (long int) pid);
+    found:;
+    }
+
+}
+
+static void
+executer_wait_cb(void *e_)
+{
+    struct executer *e = e_;
+    if (e->n_children) {
+        size_t i;
+
+        /* Wake up on SIGCHLD. */
+        poll_fd_wait(e->wait_fd, POLLIN);
+
+        /* Wake up when we get output from a child. */
+        for (i = 0; i < e->n_children; i++) {
+            struct child *child = &e->children[i];
+            if (child->output_fd >= 0) {
+                poll_fd_wait(e->wait_fd, POLLIN);
+            }
+        }
+    }
+}
+
+static void
+executer_closing_cb(struct relay *r, void *e_)
+{
+    struct executer *e = e_;
+    size_t i;
+
+    /* If any of our children was connected to 'r', then disconnect it so we
+     * don't try to reference a dead connection when the process terminates
+     * later.
+     * XXX kill the children started by 'r'? */
+    for (i = 0; i < e->n_children; i++) {
+        if (e->children[i].relay == r) {
+            e->children[i].relay = NULL;
+        }
+    }
+}
+
+static int child_fd;
+
+static void
+sigchld_handler(int signr UNUSED)
+{
+    write(child_fd, "", 1);
+}
+
+static struct hook_class executer_hook_class = {
+    NULL,                       /* local_packet_cb */
+    executer_remote_packet_cb,  /* remote_packet_cb */
+    executer_periodic_cb,       /* periodic_cb */
+    executer_wait_cb,           /* wait_cb */
+    executer_closing_cb,        /* closing_cb */
+};
+
+void
+executer_start(struct secchan *secchan, const struct settings *settings)
+{
+    struct executer *e;
+    struct sigaction sa;
+    int fds[2], null_fd;
+
+    /* Create pipe for notifying us that SIGCHLD was invoked. */
+    if (pipe(fds)) {
+        ofp_fatal(errno, "pipe failed");
+    }
+    set_nonblocking(fds[0]);
+    set_nonblocking(fds[1]);
+    child_fd = fds[1];
+
+    /* Open /dev/null. */
+    null_fd = open("/dev/null", O_RDWR);
+    if (null_fd < 0) {
+        ofp_fatal(errno, "could not open /dev/null");
+    }
+
+    /* Set up signal handler. */
+    memset(&sa, 0, sizeof sa);
+    sa.sa_handler = sigchld_handler;
+    sigemptyset(&sa.sa_mask);
+    sa.sa_flags = SA_NOCLDSTOP | SA_RESTART;
+    if (sigaction(SIGCHLD, &sa, NULL)) {
+        ofp_fatal(errno, "sigaction(SIGCHLD) faile");
+    }
+
+    /* Add hook. */
+    e = xcalloc(1, sizeof *e);
+    e->s = settings;
+    e->n_children = 0;
+    e->wait_fd = fds[0];
+    e->null_fd = null_fd;
+    add_hook(secchan, &executer_hook_class, e);
+}
diff --git a/secchan/executer.h b/secchan/executer.h
new file mode 100644 (file)
index 0000000..8e9ad3d
--- /dev/null
@@ -0,0 +1,42 @@
+/* Copyright (c) 2008 The Board of Trustees of The Leland Stanford
+ * Junior University
+ *
+ * We are making the OpenFlow specification and associated documentation
+ * (Software) available for public use and benefit with the expectation
+ * that others will use, modify and enhance the Software and contribute
+ * those enhancements back to the community. However, since we would
+ * like to make the Software available for broadest use, with as few
+ * restrictions as possible permission is hereby granted, free of
+ * charge, to any person obtaining a copy of this Software to deal in
+ * the Software under the copyrights without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT.  IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ * The name and trademarks of copyright holder(s) may NOT be used in
+ * advertising or publicity pertaining to the Software or any
+ * derivatives without specific, written prior permission.
+ */
+
+#ifndef EXECUTER_H
+#define EXECUTER_H 1
+
+struct secchan;
+struct settings;
+
+void executer_start(struct secchan *, const struct settings *);
+
+#endif /* executer.h */
index cc14d61..a6cb0f4 100644 (file)
@@ -360,6 +360,32 @@ at the switch.  The default is \fB--no-stp\fR in this distribution,
 because bugs in the STP implementation are still being worked out.
 The default will change to \fB--stp\fR at some point in the future.
 
 because bugs in the STP implementation are still being worked out.
 The default will change to \fB--stp\fR at some point in the future.
 
+.TP
+\fB--command-acl=\fR[\fB!\fR]\fIglob\fR[\fB,\fR[\fB!\fR]\fIglob\fR...]
+Configures the commands that remote OpenFlow connections are allowed
+to invoke using (e.g.) \fBdpctl execute\fR.  The argument is a
+comma-separated sequence of shell glob patterns.  A glob pattern
+specified without a leading \fB!\fR is a ``whitelist'' that specifies
+a set of commands that are that may be invoked, whereas a pattern that
+does begin with \fB!\fR is a ``blacklist'' that specifies commands
+that may not be invoked.  To be permitted, a command name must be
+whitelisted and must not be blacklisted;
+e.g. \fB--command-acl=up*,!upgrade\fR would allow any command whose name
+begins with \fBup\fR except for the command named \fBupgrade\fR.
+Command names that include characters other than upper- and lower-case
+English letters, digits, and the underscore and hyphen characters are
+unconditionally disallowed.
+
+When the whitelist and blacklist permit a command name, \fBsecchan\fR
+looks for a program with the same name as the command in the commands
+directory (see below).  Other directories are not searched.
+
+.TP
+\fB--command-dir=\fIdirectory\fR
+Sets the directory searched for remote command execution to
+\fBdirectory\fR.  The default directory is
+\fB@pkgdatadir@/commands\fR.
+
 .TP
 \fB-p\fR, \fB--private-key=\fIprivkey.pem\fR
 Specifies a PEM file containing the private key used as the switch's
 .TP
 \fB-p\fR, \fB--private-key=\fIprivkey.pem\fR
 Specifies a PEM file containing the private key used as the switch's
index 1d28eaa..345620e 100644 (file)
@@ -44,7 +44,9 @@
 #include "command-line.h"
 #include "compiler.h"
 #include "daemon.h"
 #include "command-line.h"
 #include "compiler.h"
 #include "daemon.h"
+#include "dirs.h"
 #include "discovery.h"
 #include "discovery.h"
+#include "executer.h"
 #include "fail-open.h"
 #include "fault.h"
 #include "in-band.h"
 #include "fail-open.h"
 #include "fault.h"
 #include "in-band.h"
@@ -191,6 +193,9 @@ main(int argc, char *argv[])
         rate_limit_start(&secchan, &s, switch_status,
                          local_rconn, remote_rconn);
     }
         rate_limit_start(&secchan, &s, switch_status,
                          local_rconn, remote_rconn);
     }
+    if (s.command_acl[0]) {
+        executer_start(&secchan, &s);
+    }
 
     for (;;) {
         struct relay *r, *n;
 
     for (;;) {
         struct relay *r, *n;
@@ -526,6 +531,8 @@ parse_options(int argc, char *argv[], struct settings *s)
         OPT_NO_STP,
         OPT_OUT_OF_BAND,
         OPT_IN_BAND,
         OPT_NO_STP,
         OPT_OUT_OF_BAND,
         OPT_IN_BAND,
+        OPT_COMMAND_ACL,
+        OPT_COMMAND_DIR,
         VLOG_OPTION_ENUMS
     };
     static struct option long_options[] = {
         VLOG_OPTION_ENUMS
     };
     static struct option long_options[] = {
@@ -543,6 +550,8 @@ parse_options(int argc, char *argv[], struct settings *s)
         {"no-stp",      no_argument, 0, OPT_NO_STP},
         {"out-of-band", no_argument, 0, OPT_OUT_OF_BAND},
         {"in-band",     no_argument, 0, OPT_IN_BAND},
         {"no-stp",      no_argument, 0, OPT_NO_STP},
         {"out-of-band", no_argument, 0, OPT_OUT_OF_BAND},
         {"in-band",     no_argument, 0, OPT_IN_BAND},
+        {"command-acl", required_argument, 0, OPT_COMMAND_ACL},
+        {"command-dir", required_argument, 0, OPT_COMMAND_DIR},
         {"verbose",     optional_argument, 0, 'v'},
         {"help",        no_argument, 0, 'h'},
         {"version",     no_argument, 0, 'V'},
         {"verbose",     optional_argument, 0, 'v'},
         {"help",        no_argument, 0, 'h'},
         {"version",     no_argument, 0, 'V'},
@@ -570,6 +579,8 @@ parse_options(int argc, char *argv[], struct settings *s)
     s->burst_limit = 0;
     s->enable_stp = false;
     s->in_band = true;
     s->burst_limit = 0;
     s->enable_stp = false;
     s->in_band = true;
+    s->command_acl = "";
+    s->command_dir = xasprintf("%s/commands", ofp_pkgdatadir);
     for (;;) {
         int c;
 
     for (;;) {
         int c;
 
@@ -660,6 +671,16 @@ parse_options(int argc, char *argv[], struct settings *s)
             s->in_band = true;
             break;
 
             s->in_band = true;
             break;
 
+        case OPT_COMMAND_ACL:
+            s->command_acl = (s->command_acl[0]
+                              ? xasprintf("%s,%s", s->command_acl, optarg)
+                              : optarg);
+            break;
+
+        case OPT_COMMAND_DIR:
+            s->command_dir = optarg;
+            break;
+
         case 'l':
             if (s->n_listeners >= MAX_MGMT) {
                 ofp_fatal(0,
         case 'l':
             if (s->n_listeners >= MAX_MGMT) {
                 ofp_fatal(0,
@@ -779,7 +800,11 @@ usage(void)
            "  --no-stp                disable 802.1D Spanning Tree Protocol\n"
            "\nRate-limiting of \"packet-in\" messages to the controller:\n"
            "  --rate-limit[=PACKETS]  max rate, in packets/s (default: 1000)\n"
            "  --no-stp                disable 802.1D Spanning Tree Protocol\n"
            "\nRate-limiting of \"packet-in\" messages to the controller:\n"
            "  --rate-limit[=PACKETS]  max rate, in packets/s (default: 1000)\n"
-           "  --burst-limit=BURST     limit on packet credit for idle time\n");
+           "  --burst-limit=BURST     limit on packet credit for idle time\n"
+           "\nRemote command execution options:\n"
+           "  --command-acl=[!]GLOB[,[!]GLOB...] set allowed/denied commands\n"
+           "  --command-dir=DIR       set command dir (default: %s/commands)\n",
+           ofp_pkgdatadir);
     daemon_usage();
     vlog_usage();
     printf("\nOther options:\n"
     daemon_usage();
     vlog_usage();
     printf("\nOther options:\n"
index 0a398ad..c7ead5a 100644 (file)
@@ -81,6 +81,10 @@ struct settings {
 
     /* Spanning tree protocol. */
     bool enable_stp;
 
     /* Spanning tree protocol. */
     bool enable_stp;
+
+    /* Remote command execution. */
+    char *command_acl;          /* Command white/blacklist, as shell globs. */
+    char *command_dir;          /* Directory that contains commands. */
 };
 
 struct half {
 };
 
 struct half {
index 2ab5f32..6514861 100644 (file)
@@ -198,6 +198,16 @@ messages), but it will not print any messages sent to the kernel by
 \fBsecchan\fR and other processes, nor will it print replies sent by
 the kernel in response to those messages.
 
 \fBsecchan\fR and other processes, nor will it print replies sent by
 the kernel in response to those messages.
 
+.TP
+\fBexecute \fIswitch command \fR[\fIarg\fR...]
+
+Sends a request to \fIswitch\fR to execute \fIcommand\fR along with
+each \fIarg\fR, if any, then waits for the command to complete and
+reports its completion status on \fBstderr\fR and its output, if any,
+on \fBstdout\fR.  The set of available commands and their argument is
+switch-dependent.  (This command uses a Nicira extension to OpenFlow
+that may not be available on all switches.)
+
 .PP
 The following commands can be used regardless of the connection
 method.  They apply to OpenFlow switches and controllers.
 .PP
 The following commands can be used regardless of the connection
 method.  They apply to OpenFlow switches and controllers.
index 23b9db4..bb4b87e 100644 (file)
@@ -1286,6 +1286,57 @@ do_benchmark(const struct settings *s, int argc, char *argv[])
            count * message_size / (duration / 1000.0));
 }
 
            count * message_size / (duration / 1000.0));
 }
 
+static void
+do_execute(const struct settings *s, int argc, char *argv[])
+{
+    struct vconn *vconn;
+    struct ofpbuf *request, *reply;
+    struct nicira_header *nicira;
+    struct nx_command_reply *ncr;
+    int status;
+    int i;
+
+    nicira = make_openflow(sizeof *nicira, OFPT_VENDOR, &request);
+    nicira->vendor = htonl(NX_VENDOR_ID);
+    nicira->subtype = htonl(NXT_COMMAND_REQUEST);
+    ofpbuf_put(request, argv[2], strlen(argv[2]));
+    for (i = 3; i < argc; i++) {
+        ofpbuf_put_zeros(request, 1);
+        ofpbuf_put(request, argv[i], strlen(argv[i]));
+    }
+    update_openflow_length(request);
+
+    open_vconn(argv[1], &vconn);
+    run(vconn_transact(vconn, request, &reply), "transact");
+    if (reply->size < sizeof *ncr) {
+        ofp_fatal(0, "reply is too short (%zu bytes < %zu bytes)",
+                  reply->size, sizeof *ncr);
+    }
+    ncr = reply->data;
+    if (ncr->nxh.header.type != OFPT_VENDOR
+        || ncr->nxh.vendor != htonl(NX_VENDOR_ID)
+        || ncr->nxh.subtype != htonl(NXT_COMMAND_REPLY)) {
+        ofp_fatal(0, "reply is invalid");
+    }
+
+    status = ntohl(ncr->status);
+    if (status & NXT_STATUS_EXITED) {
+        fprintf(stderr, "process terminated normally with exit code %d",
+                status & NXT_STATUS_EXITSTATUS);
+    } else if (status & NXT_STATUS_SIGNALED) {
+        fprintf(stderr, "process terminated by signal %d",
+                status & NXT_STATUS_TERMSIG);
+    } else {
+        fprintf(stderr, "process terminated for unknown reason");
+    }
+    if (status & NXT_STATUS_COREDUMP) {
+        fprintf(stderr, " (core dumped)");
+    }
+    putc('\n', stderr);
+
+    fwrite(ncr + 1, reply->size - sizeof *ncr, 1, stdout);
+}
+
 static void do_help(const struct settings *s, int argc UNUSED, 
         char *argv[] UNUSED)
 {
 static void do_help(const struct settings *s, int argc UNUSED, 
         char *argv[] UNUSED)
 {
@@ -1322,5 +1373,6 @@ static struct command all_commands[] = {
     { "probe", 1, 1, do_probe },
     { "ping", 1, 2, do_ping },
     { "benchmark", 3, 3, do_benchmark },
     { "probe", 1, 1, do_probe },
     { "ping", 1, 2, do_ping },
     { "benchmark", 3, 3, do_benchmark },
+    { "execute", 2, INT_MAX, do_execute },
     { NULL, 0, 0, NULL },
 };
     { NULL, 0, 0, NULL },
 };