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 8634a2183650c9465f02ca02cb56c685cee185a2..889445ac51402ff207b91e8bb8855c545e73f8d5 100644 (file)
@@ -34,6 +34,7 @@ TESTS =
 TESTS_ENVIRONMENT =
 bin_PROGRAMS =
 bin_SCRIPTS =
+dist_commands_DATA =
 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' \
+                -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 -*- */'
index f4e923a95ea3e11c6975f2ad91bef2cf97f78d51..4501bb864a0bc4a0ec8166c5c1e9de1ae5aa40c1 100644 (file)
@@ -53,6 +53,8 @@ OFP_CHECK_LIBOPENFLOW
 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)
index ec3696a77f59d6d0de8db018fec9c6a99b8a183c..5112332f0b9ae79bfc251983bac6337f724d3d1e 100644 (file)
@@ -99,5 +99,12 @@ SWITCH_IP=dhcp
 # 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=""
index c3d22d7d493cb1ea4c355450691d82d2403d4ac9..79457997db5cc8fb626f72c9cdf60364897d2600 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
+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
@@ -223,10 +234,16 @@ case "$1" in
             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 \
-            --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
index 2c85136180e5871ed1dc4271c29f79942331f2dc..3d2e9e9ccb9d63937ff2b5bec77e2004a1fcac4c 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/openflow/usr/share/openflow/commands usr/share/openflow/commands
index 36048cef96edc772e2f2f76f11ae6c42c8acf18f..f5de08ad8461ad04335e1115562b488c995de996 100644 (file)
@@ -34,7 +34,8 @@
 #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 */
index 4c5604e70a9ba8c9dd448f90e3de93a1394397e9..9a7a3d074db3265f7e5910022ae8a64a0f56e32a 100644 (file)
@@ -29,7 +29,16 @@ enum nicira_type {
     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 {
@@ -107,4 +116,22 @@ struct nx_action_header {
 };
 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 */
index 4ae10686556b9afbe11660e9b3079d972964e167..3816c3c4b4053ae0517e43c19ec2f765c06599ad 100644 (file)
@@ -39,6 +39,7 @@
 #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);
index 9df65e1128be8a396480f4c69bd6d7c748f559d7..2f6a67666abdd3dc1566f9c01876e7480a92b9a4 100644 (file)
@@ -9,6 +9,7 @@ VLOG_MODULE(dhcp_client)
 VLOG_MODULE(discovery)
 VLOG_MODULE(dpif)
 VLOG_MODULE(dpctl)
+VLOG_MODULE(executer)
 VLOG_MODULE(fail_open)
 VLOG_MODULE(fault)
 VLOG_MODULE(flow)
index 7b4da91189e8a90e2da99a4b662abfaf93a53516..38f58e5fec2ce193bf0fa10431076121f2c2b4c6 100644 (file)
@@ -63,6 +63,7 @@ EXTRA_DIST += \
 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
index 781bcb8fe4c7a8a71834769040d993b730328442..d8ae179b357741e99e55d607df0de8af37458e8e 100644 (file)
@@ -41,6 +41,7 @@
 #include <stddef.h>
 #include <stdio.h>
 #include <string.h>
+#include <sys/resource.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. */
index 4572e8a439b08a4d400ee072b6bc7067bc196632..d5472f051cc370762cd8f68a83afbddd42eb6a0e 100644 (file)
@@ -4,6 +4,8 @@ man_MANS += secchan/secchan.8
 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 \
@@ -13,6 +15,7 @@ secchan_secchan_SOURCES = \
        secchan/ratelimit.c \
        secchan/ratelimit.h \
        secchan/secchan.c \
+       secchan/secchan.h \
        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
+
+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 cc14d61ec48702fb0db230bb35a57b8572084b88..a6cb0f482ad0294c0033c3cbaa0e14e56f32830a 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.
 
+.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
index 1d28eaae6b03a81219e73fb921f00f2f52c0cf8c..345620e2e89434048eeaf6813fcbae0910d3bf8e 100644 (file)
@@ -44,7 +44,9 @@
 #include "command-line.h"
 #include "compiler.h"
 #include "daemon.h"
+#include "dirs.h"
 #include "discovery.h"
+#include "executer.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);
     }
+    if (s.command_acl[0]) {
+        executer_start(&secchan, &s);
+    }
 
     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_COMMAND_ACL,
+        OPT_COMMAND_DIR,
         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},
+        {"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'},
@@ -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->command_acl = "";
+    s->command_dir = xasprintf("%s/commands", ofp_pkgdatadir);
     for (;;) {
         int c;
 
@@ -660,6 +671,16 @@ parse_options(int argc, char *argv[], struct settings *s)
             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,
@@ -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"
-           "  --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"
index 0a398adc5dd2f0c337608958897d9c838a572d1a..c7ead5ab6c893aa206b9ca9932e474818ee3ab87 100644 (file)
@@ -81,6 +81,10 @@ struct settings {
 
     /* 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 {
index 2ab5f3275ac19b60258131a74e71fdbfbd7007cd..65148614b6418c352f4f48ebe06c090f61fea787 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.
 
+.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.
index 23b9db4f9868686eba9b2a640c2a33252b0fecc5..bb4b87e49da0092f74f43e413b890ec92377238b 100644 (file)
@@ -1286,6 +1286,57 @@ do_benchmark(const struct settings *s, int argc, char *argv[])
            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)
 {
@@ -1322,5 +1373,6 @@ static struct command all_commands[] = {
     { "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 },
 };