From: Ben Pfaff Date: Mon, 27 Oct 2008 19:55:55 +0000 (-0700) Subject: Add Nicira extension for remote command execution. X-Git-Url: http://git.onelab.eu/?a=commitdiff_plain;h=b2aec952c7853ac4df54aadf6097d6e930cfe1c4;p=sliver-openvswitch.git Add Nicira extension for remote command execution. --- diff --git a/Makefile.am b/Makefile.am index 8634a2183..889445ac5 100644 --- a/Makefile.am +++ b/Makefile.am @@ -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 -*- */' diff --git a/configure.ac b/configure.ac index f4e923a95..4501bb864 100644 --- a/configure.ac +++ b/configure.ac @@ -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) diff --git a/debian/openflow-switch.default b/debian/openflow-switch.default index ec3696a77..5112332f0 100644 --- a/debian/openflow-switch.default +++ b/debian/openflow-switch.default @@ -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="" diff --git a/debian/openflow-switch.init b/debian/openflow-switch.init index c3d22d7d4..79457997d 100755 --- a/debian/openflow-switch.init +++ b/debian/openflow-switch.init @@ -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 diff --git a/debian/openflow-switch.install b/debian/openflow-switch.install index 2c8513618..3d2e9e9cc 100644 --- a/debian/openflow-switch.install +++ b/debian/openflow-switch.install @@ -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 diff --git a/include/dirs.h b/include/dirs.h index 36048cef9..f5de08ad8 100644 --- a/include/dirs.h +++ b/include/dirs.h @@ -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 */ diff --git a/include/nicira-ext.h b/include/nicira-ext.h index 4c5604e70..9a7a3d074 100644 --- a/include/nicira-ext.h +++ b/include/nicira-ext.h @@ -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 */ diff --git a/include/socket-util.h b/include/socket-util.h index 4ae106865..3816c3c4b 100644 --- a/include/socket-util.h +++ b/include/socket-util.h @@ -39,6 +39,7 @@ #include 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); diff --git a/include/vlog-modules.def b/include/vlog-modules.def index 9df65e112..2f6a67666 100644 --- a/include/vlog-modules.def +++ b/include/vlog-modules.def @@ -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) diff --git a/lib/automake.mk b/lib/automake.mk index 7b4da9118..38f58e5fe 100644 --- a/lib/automake.mk +++ b/lib/automake.mk @@ -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 diff --git a/lib/socket-util.c b/lib/socket-util.c index 781bcb8fe..d8ae179b3 100644 --- a/lib/socket-util.c +++ b/lib/socket-util.c @@ -41,6 +41,7 @@ #include #include #include +#include #include #include #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. */ diff --git a/secchan/automake.mk b/secchan/automake.mk index 4572e8a43..d5472f051 100644 --- a/secchan/automake.mk +++ b/secchan/automake.mk @@ -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 index 000000000..1b291f821 --- /dev/null +++ b/secchan/commands/automake.mk @@ -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 index 000000000..8fc9314bd --- /dev/null +++ b/secchan/commands/reboot @@ -0,0 +1,2 @@ +#! /bin/sh +reboot diff --git a/secchan/commands/update b/secchan/commands/update new file mode 100755 index 000000000..4d617fea0 --- /dev/null +++ b/secchan/commands/update @@ -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 index 000000000..45f69c9df --- /dev/null +++ b/secchan/executer.c @@ -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 +#include "executer.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#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 index 000000000..8e9ad3d8f --- /dev/null +++ b/secchan/executer.h @@ -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 */ diff --git a/secchan/secchan.8.in b/secchan/secchan.8.in index cc14d61ec..a6cb0f482 100644 --- a/secchan/secchan.8.in +++ b/secchan/secchan.8.in @@ -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 diff --git a/secchan/secchan.c b/secchan/secchan.c index 1d28eaae6..345620e2e 100644 --- a/secchan/secchan.c +++ b/secchan/secchan.c @@ -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" diff --git a/secchan/secchan.h b/secchan/secchan.h index 0a398adc5..c7ead5ab6 100644 --- a/secchan/secchan.h +++ b/secchan/secchan.h @@ -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 { diff --git a/utilities/dpctl.8.in b/utilities/dpctl.8.in index 2ab5f3275..65148614b 100644 --- a/utilities/dpctl.8.in +++ b/utilities/dpctl.8.in @@ -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. diff --git a/utilities/dpctl.c b/utilities/dpctl.c index 23b9db4f9..bb4b87e49 100644 --- a/utilities/dpctl.c +++ b/utilities/dpctl.c @@ -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 }, };