From 463ee6b1b2be0ca607217f6c79012721e04e31e6 Mon Sep 17 00:00:00 2001
From: Timothy Stack <stack@flux.utah.edu>
Date: Mon, 4 Apr 2005 20:27:55 +0000
Subject: [PATCH] Mote and robot related stuff.  The main thing is the addition
 of relay capabilities to capture and related things.

	* GNUmakefile.in: Add the capture and tip subdirectories to the
	client and client-install targets.

	* configure, configure.in, config.h.in: Detect srandomdev() for
	capture and add "mote/newmote" script.

	* capture/GNUmakefile.in, capture/capture.c: Add "relay"
	capabilities to capture.

	* capture/capquery.c: Query the capserver for the relay receiver's
	port number.

	* capture/capserver.c: Small hack to return the port number
        for a node.

	* db/libdb.pm.in, db/xmlconvert.in: Add virt_tiptunnels table.

	* event/program-agent/program-agent.c: Change log file names to
	something a little more user-friendly.  Add a "MODIFY" event
	handler that lets the user set agent attributes (command, tag,
	timeout) without having to run a program.

	* event/sched/GNUmakefile.in, event/sched/console-agent.cc,
	event/sched/console-agent.h, event/sched/event-sched.c: Add
	console agents that can be used to snapshot a section of the
	capture log file.

	* event/sched/node-agent.cc: Some minor cleanup.

	* event/sched/simulator-agent.cc, event/sched/simulator-agent.h:
	Add the config data to the report mail.  Add a "RESET" event
	handler that runs "loghole clean".  Save the report mail in a file
	so it gets archived with the rest of the logs.

	* lib/libtb/tbdefs.h: Add CONSOLE object type.

	* mote/GNUmakefile.in, mote/newmote: Add newmote script, just a
	quick hack to add motes to the DB.

	* mote/tbuisp.in: Add another backend for loading motes through
	their relay capture server.

	* robots/mtp/mtp_dump.c: Dump the min/max values for x and y,
	handy for figuring out the bounds of the camera.

	* sql/database-fill.sql: Change the RELOAD-MOTE/SHUTDOWN ->
	ALWAYSUP/SHUTDOWN mode transition to ALWAYSUP/ISUP since stated
	doesn't seem to run triggers after a state change by a mode
	transition.

	* tbsetup/tbreport.in: Change the ordering of the eventlist so it
	displays event-sequences appropriately.

	* tbsetup/ns2ir/GNUmakefile.in, tbsetup/ns2ir/console.tcl,
	tbsetup/ns2ir/node.tcl, tbsetup/ns2ir/parse.tcl.in,
	tbsetup/ns2ir/sim.tcl.in: Add a "console" agent that represents
	the serial console for a node.

	* tbsetup/ns2ir/sequence.tcl: Add an "append" method so it is
	easier to build sequences dynamically.

	* tbsetup/ns2ir/topography.tcl: Make checkdest available to
	regular users.

	* tip/GNUmakefile.in, tip/tiptunnel.c: Add support for uploading a
	file to a relay version of capture and exporting the end
	connection as a pty.

	* tmcd/decls.h, tmcd/common/libsetup.pm: Bump version number since
	the dosubnodelist change is not backwards compatible.

	* tmcd/tmcd.c: Make dosubnodelist and dosubconfig callable even
	when a node isn't allocated.  Add dotiptunnels command that
	returns which serial consoles are to be mounted on a node.  Add
	mote version of subconfig that returns information needed to
	startup the relay version of capture.

	* tmcd/common/bootsubnodes: For motes, startup the relay version
	of capture (XXX stargate specific).

	* tmcd/common/libsetup.pm, tmcd/common/libtmcc.pm,
	tmcd/common/config/rc.config, tmcd/common/config/rc.tiptunnels:
	Client side changes for mounting another nodes serial line.

	* tmcd/common/rc.bootsetup: Always boot the subnodes, even when
	free.  This is used for motes since their capture needs to be up
	for reloading at the time.

	* tmcd/linux/ixpboot: Shuffle some code around so the script
	doesn't fail if the ixp isn't allocated.

	* utils/loghole.in: Add "digest.out" and "report.mail" as global
	logs to be saved in archives and display the "report.mail" file
	when showing a loghole archive.

	* xmlrpc/emulabserver.py.in: Scrub more of the return values to
	get rid of "None"s.
---
 GNUmakefile.in                      |   4 +
 capture/GNUmakefile.in              |  36 +-
 capture/capquery.c                  | 132 ++++++
 capture/capserver.c                 |  30 +-
 capture/capture.c                   | 675 ++++++++++++++++++++++------
 config.h.in                         |   2 +
 configure                           |  76 +++-
 configure.in                        |   4 +-
 db/libdb.pm.in                      |   1 +
 db/xmlconvert.in                    |   6 +-
 event/program-agent/program-agent.c |  99 ++--
 event/sched/GNUmakefile.in          |   4 +-
 event/sched/console-agent.cc        | 235 ++++++++++
 event/sched/console-agent.h         |  57 +++
 event/sched/event-sched.c           |  31 +-
 event/sched/node-agent.cc           |  10 +-
 event/sched/simulator-agent.cc      |  36 +-
 event/sched/simulator-agent.h       |   5 +
 lib/libtb/tbdefs.h                  |   1 +
 mote/GNUmakefile.in                 |   4 +-
 mote/newmote.in                     | 124 +++++
 mote/tbuisp.in                      |  13 +-
 sql/database-fill.sql               |   2 +-
 tbsetup/ns2ir/GNUmakefile.in        |   3 +-
 tbsetup/ns2ir/console.tcl           |  49 ++
 tbsetup/ns2ir/node.tcl              |  19 +-
 tbsetup/ns2ir/parse.tcl.in          |   1 +
 tbsetup/ns2ir/sequence.tcl          |  10 +
 tbsetup/ns2ir/sim.tcl.in            |  86 +++-
 tbsetup/ns2ir/topography.tcl        |  26 +-
 tbsetup/tbreport.in                 |   2 +-
 tip/GNUmakefile.in                  |  18 +-
 tip/tiptunnel.c                     | 341 +++++++++++++-
 tmcd/common/bootsubnodes            |  32 ++
 tmcd/common/config/GNUmakefile.in   |   5 +-
 tmcd/common/config/rc.config        |   4 +-
 tmcd/common/config/rc.tiptunnels    | 139 ++++++
 tmcd/common/libsetup.pm             |  47 +-
 tmcd/common/libtmcc.pm              |   3 +
 tmcd/common/rc.bootsetup            |   8 +
 tmcd/decls.h                        |   2 +-
 tmcd/linux/ixpboot                  |  12 +-
 tmcd/tmcd.c                         | 112 ++++-
 utils/GNUmakefile.in                |   2 +-
 utils/loghole.in                    |   6 +-
 xmlrpc/emulabserver.py.in           |  16 +-
 46 files changed, 2249 insertions(+), 281 deletions(-)
 create mode 100644 capture/capquery.c
 create mode 100644 event/sched/console-agent.cc
 create mode 100644 event/sched/console-agent.h
 create mode 100644 mote/newmote.in
 create mode 100644 tbsetup/ns2ir/console.tcl
 create mode 100644 tmcd/common/config/rc.tiptunnels

diff --git a/GNUmakefile.in b/GNUmakefile.in
index 98afa02a36..fe1e21aa54 100644
--- a/GNUmakefile.in
+++ b/GNUmakefile.in
@@ -114,6 +114,8 @@ ifeq ($(EVENTSYS),1)
 	@$(MAKE) -C event client
 endif
 	@$(MAKE) -C os client
+	@$(MAKE) -C capture client
+	@$(MAKE) -C tip client
 	@$(MAKE) -C sensors client
 	@$(MAKE) -C tmcd client
 
@@ -122,6 +124,8 @@ ifeq ($(EVENTSYS),1)
 	@$(MAKE) -C event client-install
 endif
 	@$(MAKE) -C os client-install
+	@$(MAKE) -C capture client-install
+	@$(MAKE) -C tip client-install
 	@$(MAKE) -C sensors client-install
 	@$(MAKE) -C tmcd client-install
 
diff --git a/capture/GNUmakefile.in b/capture/GNUmakefile.in
index 4eb1b47bab..ae882200b9 100644
--- a/capture/GNUmakefile.in
+++ b/capture/GNUmakefile.in
@@ -1,6 +1,6 @@
 #
 # EMULAB-COPYRIGHT
-# Copyright (c) 2000-2004 University of Utah and the Flux Group.
+# Copyright (c) 2000-2005 University of Utah and the Flux Group.
 # All rights reserved.
 #
 
@@ -9,11 +9,14 @@ TESTBED_SRCDIR	= @top_srcdir@
 OBJDIR		= ..
 SUBDIR		= capture
 
+SYSTEM	       := $(shell uname -s)
+
 include $(OBJDIR)/Makeconf
 
 all:		boss-all tipserv-all
 boss-all:	capserver
-tipserv-all:	capture capture-tty
+tipserv-all:	capture capture-tty capquery
+client: capture capquery
 
 include $(TESTBED_SRCDIR)/GNUmakerules
 
@@ -25,25 +28,44 @@ DESTDIR=
 # Define LOG_DROPS to record warnings in syslog whenever chars were dropped
 # due to the output device/pty being full.
 #
-CFLAGS= -g -O2 -DLOG_DROPS -I${OBJDIR} -DLOG_TESTBED=$(LOG_TESTBED)
+CFLAGS += -g -O2 -DLOG_DROPS -I${OBJDIR} -DLOG_TESTBED=$(LOG_TESTBED)
+
+ifeq ($(SYSTEM),Linux)
+ifeq ($(host_cpu),arm)
+LDFLAGS += -static
+else
+CFLAGS += -I/usr/kerberos/include
+LDFLAGS += -L/usr/kerberos/lib -lkrb5 -lk5crypto -lcom_err
+endif
+else
+LDFLAGS += -static
+endif
 
 capture: capture.c capdecls.h
-	cc -static $(CFLAGS) -DUSESOCKETS -DWITHSSL -DPREFIX=\"$(TBROOT)\" -o capture $< -lssl -lcrypto
+	$(CC) $(CFLAGS) -DUSESOCKETS -DWITHSSL -DPREFIX=\"$(TBROOT)\" -o capture $< -lssl -lcrypto $(LDFLAGS)
+
+capquery: capquery.c capdecls.h
+	$(CC) $(CFLAGS) -DPREFIX=\"$(TBROOT)\" -o $@ $<
 
 capture-nossl: capture.c capdecls.h
-	cc $(CFLAGS) -DUSESOCKETS -DPREFIX=\"$(TBROOT)\" -o capture-nossl $< 
+	$(CC) $(CFLAGS) -DUSESOCKETS -DPREFIX=\"$(TBROOT)\" -o capture-nossl $<
 
 capture-tty: capture.c capdecls.h
-	cc $(CFLAGS) -o capture-tty $<
+	$(CC) $(CFLAGS) -o capture-tty $<
 
 capserver:	capserver.c capdecls.h
-	cc $(CFLAGS) $(DBFLAGS) -o capserver $< \
+	$(CC) $(CFLAGS) $(DBFLAGS) -o capserver $< \
 		-L/usr/local/lib/mysql -lmysqlclient
 
 #
 # Do not capture install by default.
 #
 install:	all $(INSTALL_SBINDIR)/capserver
+	$(INSTALL_PROGRAM) capture $(INSTALL_DIR)/opsdir/sbin/capture
+
+client-install: client
+	$(INSTALL_PROGRAM) capture$(EXE) $(DESTDIR)$(CLIENT_BINDIR)/capture$(EXE)
+	$(INSTALL_PROGRAM) capquery$(EXE) $(DESTDIR)$(CLIENT_BINDIR)/capquery$(EXE)
 
 real-install:	all $(INSTALL_SBINDIR)/capserver $(INSTALL_SBINDIR)/capture
 
diff --git a/capture/capquery.c b/capture/capquery.c
new file mode 100644
index 0000000000..89f48d0722
--- /dev/null
+++ b/capture/capquery.c
@@ -0,0 +1,132 @@
+/*
+ * EMULAB-COPYRIGHT
+ * Copyright (c) 2005 University of Utah and the Flux Group.
+ * All rights reserved.
+ */
+
+#include "config.h"
+
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+#include <netdb.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <syslog.h>
+#include <assert.h>
+#include <stdarg.h>
+#include <sys/time.h>
+#include <grp.h>
+#include "capdecls.h"
+
+static int debug = 0;
+
+static char *usagestr = 
+ "usage: capquery [-d] [-s server] [-p #] name\n"
+ " -d		   Turn on debugging.\n"
+ " -s server	   Specify a name to connect to.\n"
+ " -p portnum	   Specify a port number to connect to.\n"
+ "\n";
+
+void
+usage()
+{
+	fprintf(stderr, usagestr);
+	exit(1);
+}
+
+static int
+mygethostbyname(struct sockaddr_in *host_addr, char *host, int port)
+{
+    struct hostent *host_ent;
+    int retval = 0;
+    
+    assert(host_addr != NULL);
+    assert(host != NULL);
+    assert(strlen(host) > 0);
+    
+    memset(host_addr, 0, sizeof(struct sockaddr_in));
+#ifndef linux
+    host_addr->sin_len = sizeof(struct sockaddr_in);
+#endif
+    host_addr->sin_family = AF_INET;
+    host_addr->sin_port = htons(port);
+    if( (host_ent = gethostbyname(host)) != NULL ) {
+        memcpy((char *)&host_addr->sin_addr.s_addr,
+               host_ent->h_addr,
+               host_ent->h_length);
+        retval = 1;
+    }
+    else {
+        retval = inet_aton(host, &host_addr->sin_addr);
+    }
+    return( retval );
+}
+
+int
+main(int argc, char **argv)
+{
+	int			sock, ch, rc, retval = EXIT_FAILURE;
+	int			length, port = SERVERPORT;
+	char		       *server = BOSSNODE;
+	struct sockaddr_in	sin;
+	whoami_t		whoami;
+
+	while ((ch = getopt(argc, argv, "ds:p:")) != -1)
+		switch(ch) {
+		case 's':
+			server = optarg;
+			break;
+		case 'p':
+			port = atoi(optarg);
+			break;
+		case 'd':
+			debug++;
+			break;
+		case 'h':
+		case '?':
+		default:
+			usage();
+		}
+	argc -= optind;
+	argv += optind;
+
+	if (argc != 1)
+		usage();
+	
+	if (strlen(argv[0]) >= sizeof(whoami.name))
+		fprintf(stderr, "Name too long: %s\n", argv[0]);
+	
+	if (getuid() != 0)
+		fprintf(stderr, "Must be run as root\n");
+
+	memset(&whoami, 0, sizeof(whoami));
+	strcpy(whoami.name, argv[0]);
+	whoami.portnum = -1;
+	
+	if (!mygethostbyname(&sin, server, port))
+		fprintf(stderr, "Bad server name: %s\n", server);
+
+	if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0)
+		perror("socket");
+	else if (bindresvport(sock, NULL) < 0)
+		perror("bindresvport");
+	else if (connect(sock, (struct sockaddr *)&sin, sizeof(sin)) < 0)
+		perror("connect");
+	else if (write(sock, &whoami, sizeof(whoami)) != sizeof(whoami))
+		perror("write");
+	else if ((rc = read(sock, &port, sizeof(port))) != sizeof(port))
+		perror("read");
+	else {
+		printf("%d\n", port);
+		retval = EXIT_SUCCESS;
+	}
+
+	close(sock);
+
+	return retval;
+}
diff --git a/capture/capserver.c b/capture/capserver.c
index 222a55d2a3..d550fe0941 100644
--- a/capture/capserver.c
+++ b/capture/capserver.c
@@ -1,6 +1,6 @@
 /*
  * EMULAB-COPYRIGHT
- * Copyright (c) 2000-2003 University of Utah and the Flux Group.
+ * Copyright (c) 2000-2003, 2005 University of Utah and the Flux Group.
  * All rights reserved.
  */
 
@@ -137,6 +137,8 @@ main(int argc, char **argv)
 		unsigned char	   buf[BUFSIZ], node_id[64];
 		secretkey_t        secretkey;
 		tipowner_t	   tipown;
+		void		  *reply = &tipown;
+		size_t		   reply_size = sizeof(tipown);
 
 		if ((clientsock = accept(tcpsock,
 					 (struct sockaddr *)&client,
@@ -193,9 +195,9 @@ main(int argc, char **argv)
 		 * message in the log file. Local tip will still work but
 		 * remote tip will not.
 		 */
-		res = mydb_query("select server,node_id from tiplines "
+		res = mydb_query("select server,node_id,portnum from tiplines "
 				 "where tipname='%s'",
-				 2, whoami.name);
+				 3, whoami.name);
 		if (!res) {
 			syslog(LOG_ERR, "DB Error getting tiplines for %s!",
 			       whoami.name);
@@ -209,6 +211,8 @@ main(int argc, char **argv)
 		}
 		row = mysql_fetch_row(res);
 		strcpy(node_id, row[1]);
+		port = -1;
+		sscanf(row[2], "%d", &port);
 		mysql_free_result(res);
 
 		/*
@@ -246,15 +250,19 @@ main(int argc, char **argv)
 		}
 		mysql_free_result(res);
 
+		if (whoami.portnum == -1) {
+			reply = &port;
+			reply_size = sizeof(port);
+		}
 		/*
 		 * Update the DB.
 		 */
-		if (mydb_update("update tiplines set portnum=%d, "
-				"keylen=%d, keydata='%s' "
-				"where tipname='%s'", 
-				whoami.portnum,
-				whoami.key.keylen, whoami.key.key,
-				whoami.name)) {
+		else if (mydb_update("update tiplines set portnum=%d, "
+				     "keylen=%d, keydata='%s' "
+				     "where tipname='%s'", 
+				     whoami.portnum,
+				     whoami.key.keylen, whoami.key.key,
+				     whoami.name)) {
 			syslog(LOG_ERR, "DB Error updating tiplines for %s!",
 			       whoami.name);
 			goto done;
@@ -263,13 +271,13 @@ main(int argc, char **argv)
 		/*
 		 * And now send the reply.
 		 */
-		if ((cc = write(clientsock, &tipown, sizeof(tipown))) <= 0) {
+		if ((cc = write(clientsock, reply, reply_size)) <= 0) {
 			if (cc < 0)
 				syslog(LOG_ERR, "Writing reply: %m");
 			syslog(LOG_ERR, "Connection aborted (write)");
 			goto done;
 		}
-		if (cc != sizeof(tipown)) {
+		if (cc != reply_size) {
 			syslog(LOG_ERR, "Wrong byte count (write)!");
 			goto done;
 		}
diff --git a/capture/capture.c b/capture/capture.c
index 91f1fd1d58..48863d127b 100644
--- a/capture/capture.c
+++ b/capture/capture.c
@@ -1,6 +1,6 @@
 /*
  * EMULAB-COPYRIGHT
- * Copyright (c) 2000-2004 University of Utah and the Flux Group.
+ * Copyright (c) 2000-2005 University of Utah and the Flux Group.
  * All rights reserved.
  */
 
@@ -30,6 +30,9 @@
 #include <errno.h>
 #include <stdlib.h>
 #include <stdarg.h>
+#include <time.h>
+#include <assert.h>
+#include <paths.h>
 
 #include <sys/param.h>
 #include <sys/file.h>
@@ -89,8 +92,10 @@ void dolog(int level, char *format, ...);
 #define DEVNAME		"%s/%s"
 #define BUFSIZE		4096
 #define DROP_THRESH	(32*1024)
-
+#define MAX_UPLOAD_SIZE	(1 * 1024 * 1024)
 #define DEFAULT_CERTFILE PREFIX"/etc/capture.pem"
+#define DEFAULT_CLIENT_CERTFILE PREFIX"/etc/client.pem"
+#define DEFAULT_CAFILE	PREFIX"/etc/emulab.pem"
 
 char 	*Progname;
 char 	*Pidname;
@@ -100,34 +105,187 @@ char	*Ttyname;
 char	*Ptyname;
 char	*Devname;
 char	*Machine;
-int	logfd, runfd, devfd, ptyfd;
+int	logfd = -1, runfd, devfd = -1, ptyfd = -1;
 int	hwflow = 0, speed = B9600, debug = 0, runfile = 0, standalone = 0;
 int	stampinterval = -1;
 sigset_t actionsigmask;
 sigset_t allsigmask;
-#ifdef  USESOCKETS
+#ifndef  USESOCKETS
+#define relay_snd 0
+#define relay_rcv 0
+#else
 char		  *Bossnode = BOSSNODE;
 struct sockaddr_in Bossaddr;
 char		  *Aclname;
 int		   serverport = SERVERPORT;
-int		   sockfd, tipactive, portnum;
+int		   sockfd, tipactive, portnum, relay_snd, relay_rcv;
+int		   upportnum = -1, upfd = -1, upfilefd = -1;
+char		   uptmpnam[64];
+size_t		   upfilesize = 0;
 struct sockaddr_in tipclient;
+struct sockaddr_in relayclient;
+struct in_addr	   relayaddr;
 secretkey_t	   secretkey;
 char		   ourhostname[MAXHOSTNAMELEN];
 int		   needshake;
 gid_t		   tipgid;
 uid_t		   tipuid;
+char		  *uploadCommand;
 
 #ifdef  WITHSSL
 
 SSL_CTX * ctx;
 SSL * sslCon;
+SSL * sslRelay;
+SSL * sslUpload;
 
 int initializedSSL = 0;
-int usingSSL = 0;
 
 const char * certfile = NULL;
+const char * cafile = NULL;
+
+int
+initializessl(void)
+{
+	static int initializedSSL = 0;
+	
+	if (initializedSSL)
+		return 0;
+	
+	SSL_load_error_strings();
+	SSL_library_init();
+	
+	ctx = SSL_CTX_new( SSLv23_method() );
+	if (ctx == NULL) {
+		dolog( LOG_NOTICE, "Failed to create context.");
+		return 1;
+	}
+	
+#ifndef PREFIX
+#define PREFIX
+#endif
+	
+	if (relay_snd) {
+		if (!cafile) { cafile = DEFAULT_CAFILE; }
+		if (SSL_CTX_load_verify_locations(ctx, cafile, NULL) == 0) {
+			die("cannot load verify locations");
+		}
+		
+		/*
+		 * Make it so the client must provide authentication.
+		 */
+		SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER |
+				   SSL_VERIFY_FAIL_IF_NO_PEER_CERT, 0);
+		
+		/*
+		 * No session caching! Useless and eats up memory.
+		 */
+		SSL_CTX_set_session_cache_mode(ctx, SSL_SESS_CACHE_OFF);
+		
+		if (!certfile) { certfile = DEFAULT_CLIENT_CERTFILE; }
+		if (SSL_CTX_use_certificate_file( ctx,
+						  certfile,
+						  SSL_FILETYPE_PEM ) <= 0) {
+			dolog(LOG_NOTICE, 
+			      "Could not load %s as certificate file.",
+			      certfile );
+			return 1;
+		}
+		
+		if (SSL_CTX_use_PrivateKey_file( ctx,
+						 certfile,
+						 SSL_FILETYPE_PEM ) <= 0) {
+			dolog(LOG_NOTICE, 
+			      "Could not load %s as key file.",
+			      certfile );
+			return 1;
+		}
+	}
+	else {
+		if (!certfile) { certfile = DEFAULT_CERTFILE; }
+		
+		if (SSL_CTX_use_certificate_file( ctx,
+						  certfile,
+						  SSL_FILETYPE_PEM ) <= 0) {
+			dolog(LOG_NOTICE, 
+			      "Could not load %s as certificate file.",
+			      certfile );
+			return 1;
+		}
+		
+		if (SSL_CTX_use_PrivateKey_file( ctx,
+						 certfile,
+						 SSL_FILETYPE_PEM ) <= 0) {
+			dolog(LOG_NOTICE, 
+			      "Could not load %s as key file.",
+			      certfile );
+			return 1;
+		}
+	}
+		
+	initializedSSL = 1;
+
+	return 0;
+}
+
+int
+sslverify(SSL *ssl, char *requiredunit)
+{
+	X509		*peer = NULL;
+	char		cname[256], unitname[256];
+	
+	assert(ssl != NULL);
+	assert(requiredunit != NULL);
+
+	if (SSL_get_verify_result(ssl) != X509_V_OK) {
+		dolog(LOG_NOTICE,
+		      "sslverify: Certificate did not verify!\n");
+		return -1;
+	}
+	
+	if (! (peer = SSL_get_peer_certificate(ssl))) {
+		dolog(LOG_NOTICE, "sslverify: No certificate presented!\n");
+		return -1;
+	}
+
+	/*
+	 * Grab stuff from the cert.
+	 */
+	X509_NAME_get_text_by_NID(X509_get_subject_name(peer),
+				  NID_organizationalUnitName,
+				  unitname, sizeof(unitname));
+
+	X509_NAME_get_text_by_NID(X509_get_subject_name(peer),
+				  NID_commonName,
+				  cname, sizeof(cname));
+	X509_free(peer);
+	
+	/*
+	 * On the server, things are a bit more difficult since
+	 * we share a common cert locally and a per group cert remotely.
+	 *
+	 * Make sure common name matches.
+	 */
+	if (strcmp(cname, BOSSNODE)) {
+		dolog(LOG_NOTICE,
+		      "sslverify: commonname mismatch: %s!=%s\n",
+		      cname, BOSSNODE);
+		return -1;
+	}
 
+	/*
+	 * If the node is remote, then the unitname must match the type.
+	 * Simply a convention. 
+	 */
+	if (strcmp(unitname, requiredunit)) {
+		dolog(LOG_NOTICE,
+		      "sslverify: unitname mismatch: %s!=Capture Server\n",
+		      unitname);
+		return -1;
+	}
+	
+	return 0;
+}
 #endif /* WITHSSL */ 
 #endif /* USESOCKETS */
 
@@ -145,7 +303,7 @@ main(int argc, char **argv)
 
 	Progname = (Progname = rindex(argv[0], '/')) ? ++Progname : *argv;
 
-	while ((op = getopt(argc, argv, "rds:Hb:ip:c:T:")) != EOF)
+	while ((op = getopt(argc, argv, "rds:Hb:ip:c:T:aou:v:")) != EOF)
 		switch (op) {
 #ifdef	USESOCKETS
 #ifdef  WITHSSL
@@ -187,6 +345,23 @@ main(int argc, char **argv)
 			if (stampinterval < 0)
 				usage();
 			break;
+#ifdef  WITHSSL
+		case 'a':
+			relay_snd = 1;
+			break;
+			
+		case 'o':
+			relay_rcv = 1;
+			break;
+			
+		case 'u':
+			uploadCommand = optarg;
+			break;
+
+		case 'v':
+			cafile = optarg;
+			break;
+#endif
 		}
 
 	argc -= optind;
@@ -232,9 +407,11 @@ main(int argc, char **argv)
 	sa.sa_mask = allsigmask;
 	sigaction(SIGINT, &sa, NULL);
 	sigaction(SIGTERM, &sa, NULL);
-	sa.sa_handler = reinit;
-	sa.sa_mask = actionsigmask;
-	sigaction(SIGHUP, &sa, NULL);
+	if (!relay_snd) {
+		sa.sa_handler = reinit;
+		sa.sa_mask = actionsigmask;
+		sigaction(SIGHUP, &sa, NULL);
+	}
 	if (runfile) {
 		sa.sa_handler = newrun;
 		sigaction(SIGUSR1, &sa, NULL);
@@ -242,16 +419,15 @@ main(int argc, char **argv)
 	sa.sa_handler = terminate;
 	sigaction(SIGUSR2, &sa, NULL);
 
+#ifdef HAVE_SRANDOMDEV
 	srandomdev();
+#else
+	srand(time(NULL));
+#endif
 	
 	/*
 	 * Open up run/log file, console tty, and controlling pty.
 	 */
-	if ((logfd = open(Logname, O_WRONLY|O_CREAT|O_APPEND, 0640)) < 0)
-		die("%s: open: %s", Logname, geterr(errno));
-	if (chmod(Logname, 0640) < 0)
-		die("%s: chmod: %s", Logname, geterr(errno));
-
 	if (runfile) {
 		unlink(Runname);
 		
@@ -313,18 +489,80 @@ main(int argc, char **argv)
 
 	createkey();
 	dolog(LOG_NOTICE, "Ready! Listening on TCP port %d", portnum);
+
+	if (relay_snd) {
+		struct sockaddr_in sin;
+		struct hostent *he;
+		secretkey_t key;
+		char *port_idx;
+		int port;
+
+		if ((port_idx = strchr(argv[0], ':')) == NULL)
+			die("%s: bad format, expecting 'host:port'", argv[0]);
+		*port_idx = '\0';
+		port_idx += 1;
+		if (sscanf(port_idx, "%d", &port) != 1)
+			die("%s: bad port number", port_idx);
+		he = gethostbyname(argv[0]);
+		if (he == 0) {
+			die("gethostbyname(%s): %s",
+			    argv[0], hstrerror(h_errno));
+		}
+		bzero(&sin, sizeof(sin));
+		memcpy ((char *)&sin.sin_addr, he->h_addr, he->h_length);
+		sin.sin_family = AF_INET;
+		sin.sin_port = htons(port);
+
+		if ((ptyfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
+			die("socket(): %s", geterr(errno));
+		if (connect(ptyfd, (struct sockaddr *)&sin, sizeof(sin)) < 0)
+			die("connect(): %s", geterr(errno));
+		sprintf(key.key, "RELAY %d", portnum);
+		key.keylen = strlen(key.key);
+		if (write(ptyfd, &key, sizeof(key)) != sizeof(key))
+			die("write(): %s", geterr(errno));
+		initializessl();
+		sslRelay = SSL_new(ctx);
+		if (!sslRelay)
+			die("SSL_new()");
+		if (SSL_set_fd(sslRelay, ptyfd) <= 0)
+			die("SSL_set_fd()");
+		if (SSL_connect(sslRelay) <= 0)
+			die("SSL_connect()");
+		if (sslverify(sslRelay, "Capture Server"))
+			die("SSL connection did not verify");
+		if (fcntl(ptyfd, F_SETFL, O_NONBLOCK) < 0)
+			die("fcntl(O_NONBLOCK): %s", geterr(errno));
+		tipactive = 1;
+	}
+
+	if (relay_rcv) {
+		struct hostent *he;
+
+		he = gethostbyname(argv[1]);
+		if (he == 0) {
+			die("gethostbyname(%s): %s",
+			    argv[1], hstrerror(h_errno));
+		}
+		memcpy ((char *)&relayaddr, he->h_addr, he->h_length);
+	}
 #else
 	if ((ptyfd = open(Ptyname, O_RDWR)) < 0)
 		die("%s: open: %s", Ptyname, geterr(errno));
 #endif
-	if ((devfd = open(Devname, O_RDWR|O_NONBLOCK)) < 0)
-		die("%s: open: %s", Devname, geterr(errno));
-
-	if (ioctl(devfd, TIOCEXCL, 0) < 0)
-		warning("TIOCEXCL %s: %s", Devname, geterr(errno));
-
+	
+	if (!relay_snd) {
+		if ((logfd = open(Logname,O_WRONLY|O_CREAT|O_APPEND,0640)) < 0)
+			die("%s: open: %s", Logname, geterr(errno));
+		if (chmod(Logname, 0640) < 0)
+			die("%s: chmod: %s", Logname, geterr(errno));
+	}
+	
+	if (!relay_rcv) {
+		rawmode(Devname, speed);
+	}
+	
 	writepid();
-	rawmode(speed);
 
 	capture();
 
@@ -443,7 +681,7 @@ capture(void)
 	 * I keep thinking (use threads) that there is a better way to do
 	 * this (use threads).  Hmm...
 	 */
-	if (fcntl(devfd, F_SETFL, O_NONBLOCK) < 0)
+	if ((devfd >= 0) && (fcntl(devfd, F_SETFL, O_NONBLOCK) < 0))
 		die("%s: fcntl(O_NONBLOCK): %s", Devname, geterr(errno));
 #ifndef USESOCKETS
 	/*
@@ -469,17 +707,19 @@ capture(void)
 #endif /* USESOCKETS */
 
 	FD_ZERO(&sfds);
-	FD_SET(devfd, &sfds);
+	if (devfd >= 0)
+		FD_SET(devfd, &sfds);
 	fdcount = devfd;
 #ifdef  USESOCKETS
 	if (devfd < sockfd)
 		fdcount = sockfd;
 	FD_SET(sockfd, &sfds);
-#else
-	if (devfd < ptyfd)
-		fdcount = ptyfd;
-	FD_SET(ptyfd, &sfds);
 #endif	/* USESOCKETS */
+	if (ptyfd >= 0) {
+		if (devfd < ptyfd)
+			fdcount = ptyfd;
+		FD_SET(ptyfd, &sfds);
+	}
 
 	fdcount++;
 
@@ -525,10 +765,25 @@ capture(void)
 		if (FD_ISSET(sockfd, &fds)) {
 			clientconnect();
 		}
+		if ((upfd >=0) && FD_ISSET(upfd, &fds)) {
+			handleupload();
+		}
 #endif	/* USESOCKETS */
-		if (FD_ISSET(devfd, &fds)) {
+		if ((devfd >= 0) && FD_ISSET(devfd, &fds)) {
 			errno = 0;
-			cc = read(devfd, buf, sizeof(buf));
+#ifdef  WITHSSL
+			if (relay_rcv) {
+			  cc = SSL_read(sslRelay, buf, sizeof(buf));
+			  if (cc <= 0) {
+			    FD_CLR(devfd, &sfds);
+			    devfd = -1;
+			    bzero(&relayclient, sizeof(relayclient));
+			    continue;
+			  }
+			}
+			else
+#endif
+			  cc = read(devfd, buf, sizeof(buf));
 			if (cc < 0)
 				die("%s: read: %s", Devname, geterr(errno));
 			if (cc == 0)
@@ -542,7 +797,10 @@ capture(void)
 #endif
 			for (lcc = 0; lcc < cc; lcc += i) {
 #ifdef  WITHSSL
-			        if (usingSSL) {
+				if (relay_snd) {
+					i = SSL_write(sslRelay, &buf[lcc], cc-lcc);
+				}
+			        else if (sslCon != NULL) {
 				        i = SSL_write(sslCon, &buf[lcc], cc-lcc);
 					if (i < 0) { i = 0; } /* XXX Hack */
 			        } else
@@ -595,11 +853,13 @@ dropped:
 				}
 				laststamp = now;
 			}
-			i = write(logfd, buf, cc);
-			if (i < 0)
-				die("%s: write: %s", Logname, geterr(errno));
-			if (i != cc)
-				die("%s: write: incomplete", Logname);
+			if (logfd >= 0) {
+				i = write(logfd, buf, cc);
+				if (i < 0)
+					die("%s: write: %s", Logname, geterr(errno));
+				if (i != cc)
+					die("%s: write: incomplete", Logname);
+			}
 			if (runfile) {
 				i = write(runfd, buf, cc);
 				if (i < 0)
@@ -611,19 +871,32 @@ dropped:
 			sigprocmask(SIG_SETMASK, &omask, NULL);
 
 		}
-		if (FD_ISSET(ptyfd, &fds)) {
+		if ((ptyfd >= 0) && FD_ISSET(ptyfd, &fds)) {
 			int lerrno;
 
 			sigprocmask(SIG_BLOCK, &actionsigmask, &omask);
 			errno = 0;
 #ifdef WITHSSL
-			if (usingSSL) {
+			if (relay_snd) {
+				cc = SSL_read( sslRelay, buf, sizeof(buf) );
+				if (cc < 0) { /* XXX hack */
+					cc = 0;
+					SSL_free(sslRelay);
+					sslRelay = NULL;
+					upportnum = -1;
+				}
+			}
+			else if (sslCon != NULL) {
 			        cc = SSL_read( sslCon, buf, sizeof(buf) );
-				if (cc < 0) { cc = 0; } /* XXX hack */
+				if (cc < 0) { /* XXX hack */
+					cc = 0;
+					SSL_free(sslCon);
+					sslCon = NULL;
+				}
 			} else
 #endif /* WITHSSL */ 
 			{
-			        cc = read(ptyfd, buf, sizeof(buf), 0);
+			        cc = read(ptyfd, buf, sizeof(buf));
 			}
 			lerrno = errno;
 			sigprocmask(SIG_SETMASK, &omask, NULL);
@@ -646,6 +919,8 @@ dropped:
 				/*
 				 * Other end disconnected.
 				 */
+				if (relay_snd)
+					die("relay receiver died");
 				dolog(LOG_INFO, "%s disconnecting",
 				      inet_ntoa(tipclient.sin_addr));
 				FD_CLR(ptyfd, &sfds);
@@ -678,7 +953,21 @@ dropped:
 
 			sigprocmask(SIG_BLOCK, &actionsigmask, &omask);
 			for (lcc = 0; lcc < cc; lcc += i) {
-				i = write(devfd, &buf[lcc], cc-lcc);
+				if (relay_rcv) {
+#ifdef USESOCKETS
+					if (sslRelay != NULL) {
+						i = SSL_write(sslRelay,
+							      &buf[lcc],
+							      cc - lcc);
+					}
+					else {
+						i = cc - lcc;
+					}
+#endif
+				}
+				else {
+					i = write(devfd, &buf[lcc], cc-lcc);
+				}
 				if (i < 0) {
 					/*
 					 * Device backed up (or FUBARed)
@@ -830,11 +1119,11 @@ terminate(int sig)
 char *optstr =
 #ifdef USESOCKETS
 #ifdef WITHSSL
-"[-c certfile] "
+"[-c certfile] [-v calist] [-u uploadcmd] "
 #endif
 "[-b bossnode] [-p bossport] [-i] "
 #endif
-"-Hdr [-s speed] [-T stampinterval]";
+"-Hdrao [-s speed] [-T stampinterval]";
 void
 usage(void)
 {
@@ -919,6 +1208,9 @@ writepid(void)
 {
 	int fd;
 	char buf[8];
+
+	if (relay_snd)
+		return;
 	
 	if ((fd = open(Pidname, O_WRONLY|O_CREAT|O_TRUNC, 0644)) < 0)
 		die("%s: open: %s", Pidname, geterr(errno));
@@ -937,10 +1229,15 @@ writepid(void)
 /*
  * Put the console line into raw mode.
  */
-rawmode(int speed)
+rawmode(char *devname, int speed)
 {
 	struct termios t;
 
+	if ((devfd = open(devname, O_RDWR|O_NONBLOCK)) < 0)
+		die("%s: open: %s", devname, geterr(errno));
+	
+	if (ioctl(devfd, TIOCEXCL, 0) < 0)
+		warning("TIOCEXCL %s: %s", Devname, geterr(errno));
 	if (tcgetattr(devfd, &t) < 0)
 		die("%s: tcgetattr: %s", Devname, geterr(errno));
 	(void) cfsetispeed(&t, speed);
@@ -1051,148 +1348,193 @@ val2speed(int val)
 int
 clientconnect(void)
 {
-	int		cc, length = sizeof(tipclient);
+	struct sockaddr_in sin;
+	int		cc, length = sizeof(sin);
+	int             dorelay = 0, doupload = 0;
 	int             ret;
 	int		newfd;
 	secretkey_t     key;
 	capret_t	capret;
+	SSL	       *newssl;
 
-	newfd = accept(sockfd, (struct sockaddr *)&tipclient, &length);
+	newfd = accept(sockfd, (struct sockaddr *)&sin, &length);
 	if (newfd < 0) {
 		dolog(LOG_NOTICE, "accept()ing new client: %s", geterr(errno));
 		return 1;
 	}
 
-	/*
-	 * Is there a better way to do this? I suppose we
-	 * could shut the main socket down, and recreate
-	 * it later when the client disconnects, but that
-	 * sounds horribly brutish!
-	 */
-	if (tipactive) {
-		capret = CAPBUSY;
-		if ((cc = write(newfd, &capret, sizeof(capret))) <= 0) {
-			dolog(LOG_NOTICE, "%s refusing. error writing status",
-			      inet_ntoa(tipclient.sin_addr));
-		}
-		dolog(LOG_NOTICE, "%s connecting, but tip is active",
-		      inet_ntoa(tipclient.sin_addr));
-		
-		close(newfd);
-		return 1;
-	}
-	ptyfd = newfd;
-
 	/*
 	 * Read the first part to verify the key. We must get the
 	 * proper bits or this is not a valid tip connection.
 	 */
-	if ((cc = read(ptyfd, &key, sizeof(key))) <= 0) {
-		close(ptyfd);
+	if ((cc = read(newfd, &key, sizeof(key))) <= 0) {
+		close(newfd);
 		dolog(LOG_NOTICE, "%s connecting, error reading key",
-		      inet_ntoa(tipclient.sin_addr));
+		      inet_ntoa(sin.sin_addr));
 		return 1;
 	}
 
 #ifdef WITHSSL
-	usingSSL = 0;
-
 	if (cc == sizeof(key) && 
-	    0 == strncmp( key.key, "USESSL", 6 )) {
-	  usingSSL = 1;
+	    (0 == strncmp( key.key, "USESSL", 6 ) ||
+	     (dorelay = (0 == strncmp( key.key, "RELAY", 5 ))) ||
+	     (doupload = (0 == strncmp( key.key, "UPLOAD", 6 ))))) {
 	  /* 
 	     dolog(LOG_NOTICE, "Client %s wants to use SSL",
-		inet_ntoa(tipclient.sin_addr) );
+		inet_ntoa(sin.sin_addr) );
 	  */
 
-	  if (!initializedSSL) {
-	    SSL_load_error_strings();
-	    SSL_library_init();
-
-	    ctx = SSL_CTX_new( SSLv23_method() );
-	    if (ctx == NULL) {
-	      dolog( LOG_NOTICE, "Failed to create context.");
-	      close( ptyfd );
-	      return 1;
-	    }
-
-#ifndef PREFIX
-#define PREFIX
-#endif
-
-	    if (!certfile) { certfile = DEFAULT_CERTFILE; }
-
-	    if (SSL_CTX_use_certificate_file( ctx, certfile, SSL_FILETYPE_PEM )
-		<= 0) {
-	      dolog(LOG_NOTICE, 
-		    "Could not load %s as certificate file.",
-		    certfile );
-	      close(ptyfd);
-	      return 1;
-	    }
-
-	    if (SSL_CTX_use_PrivateKey_file( ctx, certfile, SSL_FILETYPE_PEM )
-		<= 0) {
-	      dolog(LOG_NOTICE, 
-		    "Could not load %s as key file.",
-		    certfile );
-	      close(ptyfd);
-	      return 1;
-	    }
-
-	    initializedSSL = 1;
-	  }
+	  initializessl();
 	  /*
-	  if ( write( ptyfd, "OKAY", 4 ) <= 0) {
+	  if ( write( newfd, "OKAY", 4 ) <= 0) {
 	    dolog( LOG_NOTICE, "Failed to send OKAY to client." );
-	    close( ptyfd );
+	    close( newfd );
 	    return 1;
 	  }
 	  */
 
-	  sslCon = SSL_new( ctx );
-	  if (!sslCon) {
+	  newssl = SSL_new( ctx );
+	  if (!newssl) {
 	    dolog(LOG_NOTICE, "SSL_new failed.");
-	    close(ptyfd);
+	    close(newfd);
 	    return 1;
 	  }	    
 	    
-	  if ((ret = SSL_set_fd( sslCon, ptyfd )) <= 0) {
+	  if ((ret = SSL_set_fd( newssl, newfd )) <= 0) {
 	    dolog(LOG_NOTICE, "SSL_set_fd failed.");
-	    close(ptyfd);
+	    close(newfd);
 	    return 1;
 	  }
 
 	  dolog(LOG_NOTICE, "going to accept" );
 
-	  if ((ret = SSL_accept( sslCon )) <= 0) {
+	  if ((ret = SSL_accept( newssl )) <= 0) {
 	    dolog(LOG_NOTICE, "%s connecting, SSL_accept error.",
-		  inet_ntoa(tipclient.sin_addr));
-	    goto sslerror;
+		  inet_ntoa(sin.sin_addr));
+	    ERR_print_errors_fp( stderr );
+	    SSL_free(newssl);
+	    close(newfd);
+	    return 1;
 	  }
 
-	  dolog(LOG_NOTICE, "going to read key" );
-
-	  if ((cc = SSL_read(sslCon, (void *)&key, sizeof(key))) <= 0) {
-	    ret = cc;
-	    close(ptyfd);
-	    dolog(LOG_NOTICE, "%s connecting, error reading capturekey.",
-		  inet_ntoa(tipclient.sin_addr));
-	  sslerror:
-	    /*
-	    {
-	      FILE * foo = fopen("/tmp/err.txt", "w");
-	      ERR_print_errors_fp( foo );
-	      fclose( foo );
+	  if (doupload) {
+	    strcpy(uptmpnam, _PATH_TMP "capture.upload.XXXXXX");
+	    if (upfd >= 0 || !relay_snd || !uploadCommand) {
+	      dolog(LOG_NOTICE, "%s upload already connected.",
+		    inet_ntoa(sin.sin_addr));
+	      SSL_free(newssl);
+	      close(newfd);
+	      return 1;
+	    }
+	    else if (sslverify(newssl, "Capture Server")) {
+	      SSL_free(newssl);
+	      close(newfd);
+	      return 1;
+	    }
+	    else if ((upfilefd = mkstemp(uptmpnam)) < 0) {
+	      dolog(LOG_NOTICE, "failed to create upload file");
+	      printf(" %s\n", uptmpnam);
+	      perror("mkstemp");
+	      SSL_free(newssl);
+	      close(newfd);
+	      return 1;
+	    }
+	    else {
+	      upfd = newfd;
+	      upfilesize = 0;
+	      FD_SET(upfd, &sfds);
+	      if (upfd >= fdcount) {
+		fdcount = upfd;
+		fdcount += 1;
+	      }
+	      sslUpload = newssl;
+	      if (fcntl(upfd, F_SETFL, O_NONBLOCK) < 0)
+		die("fcntl(O_NONBLOCK): %s", geterr(errno));
+	      return 0;
+	    }
+	  }
+	  else if (dorelay) {
+	    if (devfd >= 0) {
+	      dolog(LOG_NOTICE, "%s relay already connected.",
+		    inet_ntoa(sin.sin_addr));
+	      SSL_free(newssl);
+	      shutdown(newfd, SHUT_RDWR);
+	      close(newfd);
+	      return 1;
+	    }
+	    else if (memcmp(&relayaddr,
+			    &sin.sin_addr,
+			    sizeof(relayaddr)) != 0) {
+	      dolog(LOG_NOTICE, "%s is not the relay host.",
+		    inet_ntoa(sin.sin_addr));
+	      SSL_free(newssl);
+	      shutdown(newfd, SHUT_RDWR);
+	      close(newfd);
+	      return 1;
+	    }
+	    else {
+	      relayclient = sin;
+	      devfd = newfd;
+	      sscanf(key.key, "RELAY %d", &upportnum);
+	      FD_SET(devfd, &sfds);
+	      if (devfd >= fdcount) {
+		fdcount = devfd;
+		fdcount += 1;
+	      }
+	      sslRelay = newssl;
+	      if (fcntl(devfd, F_SETFL, O_NONBLOCK) < 0)
+		die("fcntl(O_NONBLOCK): %s", geterr(errno));
+	      createkey();
+	      return 0;
+	    }
+	  }
+	  else if (!tipactive) {
+	    sslCon = newssl;
+	    tipclient = sin;
+	    ptyfd = newfd;
+	    dolog(LOG_NOTICE, "going to read key" );
+	    if ((cc = SSL_read(newssl, (void *)&key, sizeof(key))) <= 0) {
+	      ret = cc;
+	      close(newfd);
+	      dolog(LOG_NOTICE, "%s connecting, error reading capturekey.",
+		    inet_ntoa(sin.sin_addr));
+	      /*
+		{
+		FILE * foo = fopen("/tmp/err.txt", "w");
+		ERR_print_errors_fp( foo );
+		fclose( foo );
+		}
+	      */
+	      close(ptyfd);
+	      return 1;
 	    }
-	    */
-	    close(ptyfd);
-	    return 1;
 	  }
 
 	  dolog(LOG_NOTICE, "got key" );
 	}
+	else if (!tipactive) {
+		tipclient = sin;
+		ptyfd = newfd;
+	}
 #endif /* WITHSSL */
+	/*
+	 * Is there a better way to do this? I suppose we
+	 * could shut the main socket down, and recreate
+	 * it later when the client disconnects, but that
+	 * sounds horribly brutish!
+	 */
+	if (tipactive) {
+		capret = CAPBUSY;
+		if ((cc = write(newfd, &capret, sizeof(capret))) <= 0) {
+			dolog(LOG_NOTICE, "%s refusing. error writing status",
+			      inet_ntoa(tipclient.sin_addr));
+		}
+		dolog(LOG_NOTICE, "%s connecting, but tip is active",
+		      inet_ntoa(tipclient.sin_addr));
+		
+		close(newfd);
+		return 1;
+	}
 	/* Verify size of the key is sane */
 	if (cc != sizeof(key) ||
 	    key.keylen != strlen(key.key) ||
@@ -1202,7 +1544,7 @@ clientconnect(void)
 		 */
 		capret = CAPNOPERM;
 #ifdef WITHSSL
-		if (usingSSL) {
+		if (sslCon != NULL) {
 		    if ((cc = SSL_write(sslCon, (void *)&capret, sizeof(capret))) <= 0) {
 		        dolog(LOG_NOTICE, "%s connecting, error perm status",
 			      inet_ntoa(tipclient.sin_addr));
@@ -1226,12 +1568,13 @@ clientconnect(void)
 	dolog(LOG_INFO, "Key: %d: %s",
 	      secretkey.keylen, secretkey.key);
 #endif
+
 	/*
 	 * Tell the other side that all is okay.
 	 */
 	capret = CAPOK;
 #ifdef WITHSSL
-	if (usingSSL) {
+	if (sslCon != NULL) {
 	    if ((cc = SSL_write(sslCon, (void *)&capret, sizeof(capret))) <= 0) {
 		close(ptyfd);
 		dolog(LOG_NOTICE, "%s connecting, error writing status",
@@ -1266,6 +1609,49 @@ clientconnect(void)
 	return 0;
 }
 
+int
+handleupload(void)
+{
+	int		drop = 0, rc, retval = 0;
+	char		buffer[BUFSIZE];
+
+	if ((rc = SSL_read(sslUpload, buffer, sizeof(buffer))) < 0) {
+		if ((errno != EINTR) && (errno != EAGAIN)) {
+			drop = 1;
+		}
+	}
+	else if ((upfilesize + rc) > MAX_UPLOAD_SIZE) {
+		dolog(LOG_NOTICE, "upload to large");
+		drop = 1;
+	}
+	else if (rc == 0) {
+		snprintf(buffer, sizeof(buffer), uploadCommand, uptmpnam);
+		dolog(LOG_NOTICE, "upload done");
+		drop = 1;
+		close(devfd);
+		/* XXX run uisp */
+		system(buffer);
+		rawmode(Devname, speed);
+	}
+	else {
+		write(upfilefd, buffer, rc);
+		upfilesize += rc;
+	}
+
+	if (drop) {
+		SSL_free(sslUpload);
+		sslUpload = NULL;
+		FD_CLR(upfd, &sfds);
+		close(upfd);
+		upfd = -1;
+		close(upfilefd);
+		upfilefd = -1;
+		unlink(uptmpnam);
+	}
+	
+	return retval;
+}
+
 /*
  * Generate our secret key and write out the file that local tip uses
  * to do a secure connect.
@@ -1277,6 +1663,9 @@ createkey(void)
 	unsigned char		buf[BUFSIZ];
 	FILE		       *fp;
 
+	if (relay_snd)
+		return 1;
+
 	/*
 	 * Generate the key. Should probably generate a random
 	 * number of random bits ...
@@ -1343,6 +1732,10 @@ createkey(void)
 
 	fprintf(fp, "host:   %s\n", ourhostname);
 	fprintf(fp, "port:   %d\n", portnum);
+	if (upportnum > 0) {
+		fprintf(fp, "uphost: %s\n", inet_ntoa(relayaddr));
+		fprintf(fp, "upport: %d\n", upportnum);
+	}
 	fprintf(fp, "keylen: %d\n", secretkey.keylen);
 	fprintf(fp, "key:    %s\n", secretkey.key);
 	fclose(fp);
@@ -1384,7 +1777,7 @@ handshake(void)
 	/*
 	 * In standalone, do not contact the capserver.
 	 */
-	if (standalone)
+	if (standalone || relay_snd)
 		return err;
 
 	/*
@@ -1420,7 +1813,7 @@ handshake(void)
 	 * number does not matter.
 	 */
 	if (bindresvport(sock, NULL) < 0) {
-		warnc("Could not bind reserved port");
+		warning("Could not bind reserved port");
 		close(sock);
 		return -1;
 	}
diff --git a/config.h.in b/config.h.in
index 342c3febf7..e7df8b1f1a 100644
--- a/config.h.in
+++ b/config.h.in
@@ -30,5 +30,7 @@
 #undef EVENTSERVER
 #undef BOSSEVENTPORT
 
+#undef HAVE_SRANDOMDEV
+
 #undef HAVE_LINUX_VIDEODEV_H
 #undef HAVE_MEZZANINE
diff --git a/configure b/configure
index d608bd8757..2dc077c00b 100755
--- a/configure
+++ b/configure
@@ -1222,6 +1222,62 @@ fi
 
 
 
+for ac_func in srandomdev
+do
+echo $ac_n "checking for $ac_func""... $ac_c" 1>&6
+echo "configure:1229: checking for $ac_func" >&5
+if eval "test \"`echo '$''{'ac_cv_func_$ac_func'+set}'`\" = set"; then
+  echo $ac_n "(cached) $ac_c" 1>&6
+else
+  cat > conftest.$ac_ext <<EOF
+#line 1234 "configure"
+#include "confdefs.h"
+/* System header to define __stub macros and hopefully few prototypes,
+    which can conflict with char $ac_func(); below.  */
+#include <assert.h>
+/* Override any gcc2 internal prototype to avoid an error.  */
+/* We use char because int might match the return type of a gcc2
+    builtin and then its argument prototype would still apply.  */
+char $ac_func();
+
+int main() {
+
+/* The GNU C library defines this for functions which it implements
+    to always fail with ENOSYS.  Some functions are actually named
+    something starting with __ and the normal name is an alias.  */
+#if defined (__stub_$ac_func) || defined (__stub___$ac_func)
+choke me
+#else
+$ac_func();
+#endif
+
+; return 0; }
+EOF
+if { (eval echo configure:1257: \"$ac_link\") 1>&5; (eval $ac_link) 2>&5; } && test -s conftest${ac_exeext}; then
+  rm -rf conftest*
+  eval "ac_cv_func_$ac_func=yes"
+else
+  echo "configure: failed program was:" >&5
+  cat conftest.$ac_ext >&5
+  rm -rf conftest*
+  eval "ac_cv_func_$ac_func=no"
+fi
+rm -f conftest*
+fi
+
+if eval "test \"`echo '$ac_cv_func_'$ac_func`\" = yes"; then
+  echo "$ac_t""yes" 1>&6
+    ac_tr_func=HAVE_`echo $ac_func | tr 'abcdefghijklmnopqrstuvwxyz' 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'`
+  cat >> confdefs.h <<EOF
+#define $ac_tr_func 1
+EOF
+ 
+else
+  echo "$ac_t""no" 1>&6
+fi
+done
+
+
 
 
 #
@@ -1854,17 +1910,17 @@ for ac_hdr in ulxmlrpcpp/ulxr_config.h
 do
 ac_safe=`echo "$ac_hdr" | sed 'y%./+-%__p_%'`
 echo $ac_n "checking for $ac_hdr""... $ac_c" 1>&6
-echo "configure:1858: checking for $ac_hdr" >&5
+echo "configure:1914: checking for $ac_hdr" >&5
 if eval "test \"`echo '$''{'ac_cv_header_$ac_safe'+set}'`\" = set"; then
   echo $ac_n "(cached) $ac_c" 1>&6
 else
   cat > conftest.$ac_ext <<EOF
-#line 1863 "configure"
+#line 1919 "configure"
 #include "confdefs.h"
 #include <$ac_hdr>
 EOF
 ac_try="$ac_cpp conftest.$ac_ext >/dev/null 2>conftest.out"
-{ (eval echo configure:1868: \"$ac_try\") 1>&5; (eval $ac_try) 2>&5; }
+{ (eval echo configure:1924: \"$ac_try\") 1>&5; (eval $ac_try) 2>&5; }
 ac_err=`grep -v '^ *+' conftest.out | grep -v "^conftest.${ac_ext}\$"`
 if test -z "$ac_err"; then
   rm -rf conftest*
@@ -1903,17 +1959,17 @@ for ac_hdr in linux/videodev.h
 do
 ac_safe=`echo "$ac_hdr" | sed 'y%./+-%__p_%'`
 echo $ac_n "checking for $ac_hdr""... $ac_c" 1>&6
-echo "configure:1907: checking for $ac_hdr" >&5
+echo "configure:1963: checking for $ac_hdr" >&5
 if eval "test \"`echo '$''{'ac_cv_header_$ac_safe'+set}'`\" = set"; then
   echo $ac_n "(cached) $ac_c" 1>&6
 else
   cat > conftest.$ac_ext <<EOF
-#line 1912 "configure"
+#line 1968 "configure"
 #include "confdefs.h"
 #include <$ac_hdr>
 EOF
 ac_try="$ac_cpp conftest.$ac_ext >/dev/null 2>conftest.out"
-{ (eval echo configure:1917: \"$ac_try\") 1>&5; (eval $ac_try) 2>&5; }
+{ (eval echo configure:1973: \"$ac_try\") 1>&5; (eval $ac_try) 2>&5; }
 ac_err=`grep -v '^ *+' conftest.out | grep -v "^conftest.${ac_ext}\$"`
 if test -z "$ac_err"; then
   rm -rf conftest*
@@ -1946,7 +2002,7 @@ done
 # Extract the first word of "gtk-config", so it can be a program name with args.
 set dummy gtk-config; ac_word=$2
 echo $ac_n "checking for $ac_word""... $ac_c" 1>&6
-echo "configure:1950: checking for $ac_word" >&5
+echo "configure:2006: checking for $ac_word" >&5
 if eval "test \"`echo '$''{'ac_cv_prog_GTK_CONFIG'+set}'`\" = set"; then
   echo $ac_n "(cached) $ac_c" 1>&6
 else
@@ -2025,7 +2081,7 @@ fi
 # SVR4 /usr/ucb/install, which tries to use the nonexistent group "staff"
 # ./install, which can be erroneously created by make from ./install.sh.
 echo $ac_n "checking for a BSD compatible install""... $ac_c" 1>&6
-echo "configure:2029: checking for a BSD compatible install" >&5
+echo "configure:2085: checking for a BSD compatible install" >&5
 if test -z "$INSTALL"; then
 if eval "test \"`echo '$''{'ac_cv_path_install'+set}'`\" = set"; then
   echo $ac_n "(cached) $ac_c" 1>&6
@@ -2086,7 +2142,7 @@ esac
 # Extract the first word of "rsync", so it can be a program name with args.
 set dummy rsync; ac_word=$2
 echo $ac_n "checking for $ac_word""... $ac_c" 1>&6
-echo "configure:2090: checking for $ac_word" >&5
+echo "configure:2146: checking for $ac_word" >&5
 if eval "test \"`echo '$''{'ac_cv_path_RSYNC'+set}'`\" = set"; then
   echo $ac_n "(cached) $ac_c" 1>&6
 else
@@ -2256,7 +2312,7 @@ outfiles="$outfiles Makeconf GNUmakefile \
 	dhcpd/dhcpd.conf.template dhcpd/GNUmakefile \
 	install/GNUmakefile install/ops-install install/boss-install \
 	install/newnode_sshkeys/GNUmakefile install/smb.conf.head \
-	mote/GNUmakefile mote/tbuisp mote/tbsgmotepower \
+	mote/GNUmakefile mote/tbuisp mote/tbsgmotepower mote/newmote \
 	robots/GNUmakefile robots/tbsetdest/GNUmakefile \
 	robots/mtp/GNUmakefile robots/emc/GNUmakefile \
 	robots/emc/test_emcd.sh robots/emc/loclistener \
diff --git a/configure.in b/configure.in
index 28abd1c969..6be143df31 100755
--- a/configure.in
+++ b/configure.in
@@ -57,6 +57,8 @@ AC_PATH_PROG(JAR,jar)
 
 AC_CHECK_TOOL(SSH,ssh)
 
+AC_CHECK_FUNCS(srandomdev)
+
 AC_SUBST(optional_subdirs)
 
 #
@@ -751,7 +753,7 @@ outfiles="$outfiles Makeconf GNUmakefile \
 	dhcpd/dhcpd.conf.template dhcpd/GNUmakefile \
 	install/GNUmakefile install/ops-install install/boss-install \
 	install/newnode_sshkeys/GNUmakefile install/smb.conf.head \
-	mote/GNUmakefile mote/tbuisp mote/tbsgmotepower \
+	mote/GNUmakefile mote/tbuisp mote/tbsgmotepower mote/newmote \
 	robots/GNUmakefile robots/tbsetdest/GNUmakefile \
 	robots/mtp/GNUmakefile robots/emc/GNUmakefile \
 	robots/emc/test_emcd.sh robots/emc/loclistener \
diff --git a/db/libdb.pm.in b/db/libdb.pm.in
index 946f72b3d7..d01dfd8586 100644
--- a/db/libdb.pm.in
+++ b/db/libdb.pm.in
@@ -3144,6 +3144,7 @@ sub TBRobotLabExpt($$)
 		   "event_groups",
 		   "firewalls",
 		   "firewall_rules",
+		   "virt_tiptunnels",
 		   "ipsubnets",
 		   "nsfiles");
 
diff --git a/db/xmlconvert.in b/db/xmlconvert.in
index 42ae982bdd..3959b1415c 100644
--- a/db/xmlconvert.in
+++ b/db/xmlconvert.in
@@ -130,7 +130,11 @@ my %virtual_tables =
      "firewall_rules"		=> { rows  => undef, 
 				     tag   => "firewall_rules",
 				     row   => "firewall_rule",
-				     attrs => [ "fwname", "ruleno", "rule" ]}
+				     attrs => [ "fwname", "ruleno", "rule" ]},
+     "virt_tiptunnels"		=> { rows  => undef, 
+				     tag   => "tiptunnels",
+				     row   => "tiptunnel",
+				     attrs => [ "host", "vnode" ]}
      );
 
 # XXX
diff --git a/event/program-agent/program-agent.c b/event/program-agent/program-agent.c
index bc4d4c72fd..19a426d80c 100644
--- a/event/program-agent/program-agent.c
+++ b/event/program-agent/program-agent.c
@@ -182,6 +182,8 @@ static void	start_program(struct proginfo *pinfo,
 			      unsigned long token,
 			      char *args);
 
+static void	set_program(struct proginfo *pinfo, char *args);
+
 /**
  * Stop a running program.
  *
@@ -825,12 +827,40 @@ callback(event_handle_t handle, event_notification_t notification, void *data)
 	else if (strcmp(event, TBDB_EVENTTYPE_KILL) == 0) {
 		signal_program(pinfo, args);
 	}
+	else if (strcmp(event, TBDB_EVENTTYPE_MODIFY) == 0) {
+		set_program(pinfo, args);
+	}
 	else {
 		error("Invalid event: %s\n", event);
 		return;
 	}
 }
 
+static char *fileext(char *path)
+{
+    int has_token = 0, lpc, len;
+    char *retval = NULL;
+    
+    assert(path != NULL);
+
+    len = strlen(path);
+    for (lpc = len - 1; lpc > 0; lpc--) {
+	if (path[lpc] == '.') {
+	    if (has_token) {
+		retval = &path[lpc + 1];
+	    }
+	    else if (sscanf(&path[lpc + 1], "%*d") == 1) {
+		has_token = 1;
+	    }
+	    else {
+		retval = &path[lpc + 1];
+	    }
+	}
+    }
+
+    return retval;
+}
+
 static void
 start_callback(event_handle_t handle,
 	       event_notification_t notification,
@@ -880,20 +910,17 @@ start_callback(event_handle_t handle,
 			char path[1024];
 
 			while ((de = readdir(dir)) != NULL) {
-				unsigned int token = 0;
-				char ext[16];
+				char *ext = NULL;
 
 				if ((strlen(de->d_name) < sizeof(path)) &&
-				    ((sscanf(de->d_name,
-					     "%[^.].%u.%16s",
-					     path, &token, ext) == 3) ||
-				     (sscanf(de->d_name,
-					     "%[^.].%16s",
-					     path, ext) == 2)) &&
+				    (sscanf(de->d_name,
+					    "%1024[^.].",
+					    path) == 1) &&
 				    (find_agent(path) != NULL) &&
-				    ((strcmp(ext, "out") == 0) ||
-				     (strcmp(ext, "err") == 0) ||
-				     (strcmp(ext, "status") == 0))) {
+				    ((ext = fileext(de->d_name)) != NULL) &&
+				    ((strncmp(ext, "out", 3) == 0) ||
+				     (strncmp(ext, "err", 3) == 0) ||
+				     (strncmp(ext, "status", 6) == 0))) {
 					snprintf(path,
 						 sizeof(path),
 						 "%s/%s",
@@ -940,16 +967,16 @@ open_logfile(struct proginfo *pinfo, const char *type)
 	 * agent name, the event token, and the type (e.g. out, err).
 	 */
 	snprintf(buf, sizeof(buf),
-		 "%s/%s.%lu.%s",
-		 LOGDIR, pinfo->name, pinfo->token, type);
+		 "%s/%s.%s.%lu",
+		 LOGDIR, pinfo->name, type, pinfo->token);
 	if ((retval = open(buf, O_WRONLY|O_CREAT|O_APPEND, 0640)) >= 0) {
 		/*
 		 * We've successfully created the file, now create the
 		 * symlinks to that refer to the last run and a tagged run.
 		 */
 		snprintf(buf, sizeof(buf),
-			 "./%s.%lu.%s",
-			 pinfo->name, pinfo->token, type);
+			 "./%s.%s.%lu",
+			 pinfo->name, type, pinfo->token);
 		snprintf(buf2, sizeof(buf2),
 			 "%s/%s.%s",
 			 LOGDIR, pinfo->name, type);
@@ -968,16 +995,11 @@ open_logfile(struct proginfo *pinfo, const char *type)
 }
 
 static void
-start_program(struct proginfo *pinfo, unsigned long token, char *args)
+set_program(struct proginfo *pinfo, char *args)
 {
-	int		pid, in_fd, out_fd = -1, err_fd = -1;
+	assert(pinfo != NULL);
+	assert(args != NULL);
 	
-	if (pinfo->pid != 0) {
-		warning("start_program: %s is still running: %d\n",
-			pinfo->name, pinfo->pid);
-		return;
-	}
-
 	/*
 	 * The args string holds the command line to execute. We allow
 	 * this to be reset in dynamic events, but is optional; the cuurent
@@ -1043,17 +1065,30 @@ start_program(struct proginfo *pinfo, unsigned long token, char *args)
 			value = NULL;
 		}
 	}
+}
+
+static void
+start_program(struct proginfo *pinfo, unsigned long token, char *args)
+{
+	int		pid, in_fd, out_fd = -1, err_fd = -1;
+	
+	if (pinfo->pid != 0) {
+		warning("start_program: %s is still running: %d\n",
+			pinfo->name, pinfo->pid);
+		return;
+	}
+
+	set_program(pinfo, args);
 
 	gettimeofday(&pinfo->started, NULL);
 	pinfo->token = token;
 
-
 	if ((pinfo->timeout > 0) &&
 	    (pinfo->timeout_handle =
 	     elvin_sync_add_timeout(NULL,
 				    pinfo->timeout * 1000,
 				    timeout_callback,
-					 pinfo,
+				    pinfo,
 				    elvin_error)) == NULL) {
 		error("Could not add timeout for %s!", pinfo->name);
 	}
@@ -1525,13 +1560,16 @@ child_callback(elvin_io_handler_t handler,
 				}
 			}
 			else {
-				exit_code = status;
+				if (status == pi->expected_exit_code)
+					exit_code = 0;
+				else
+					exit_code = status;
 			}
 
 			/* Dump a status file and */
 			snprintf(path,
 				 sizeof(path),
-				 "%s/%s.%lu.status",
+				 "%s/%s.status.%lu",
 				 LOGDIR,
 				 pi->name,
 				 pi->token);
@@ -1580,6 +1618,11 @@ child_callback(elvin_io_handler_t handler,
 					ru.ru_maxrss);
 				fclose(file);
 			}
+			snprintf(path,
+				 sizeof(path),
+				 "./%s.status.%lu",
+				 pi->name,
+				 pi->token);
 			snprintf(path2,
 				 sizeof(path),
 				 "%s/%s.status",
@@ -1591,7 +1634,7 @@ child_callback(elvin_io_handler_t handler,
 				snprintf(path2,
 					 sizeof(path),
 					 "%s/%s.%s.status",
-					 LOGDIR, pi->tag, pi->name);
+					 LOGDIR, pi->name, pi->tag);
 				unlink(path2);
 				symlink(path, path2);
 			}
diff --git a/event/sched/GNUmakefile.in b/event/sched/GNUmakefile.in
index 614a99bfa7..739885bb97 100644
--- a/event/sched/GNUmakefile.in
+++ b/event/sched/GNUmakefile.in
@@ -46,6 +46,7 @@ version.c: event-sched.c
 	echo >$@ "char build_info[] = \"Built on `date +%d-%b-%Y` by `id -nu`@`hostname | sed 's/\..*//'`:`pwd`\";"
 
 OBJS = \
+	console-agent.o \
 	error-record.o \
 	event-sched_rpc.o \
 	group-agent.o \
@@ -62,7 +63,7 @@ event-sched_rrpc: $(OBJS) event-sched.h ../lib/libevent.a
 	$(CXX) $(CFLAGS) -static $(LDFLAGS) -o $@ $(OBJS) $(ULXRLIBS) $(LIBS)
 
 DEPS = \
-	error-record.h event-sched.h group-agent.h listNode.h \
+	console-agent.h error-record.h event-sched.h group-agent.h listNode.h \
 	local-agent.h node-agent.h rpc.h simulator-agent.h timeline-agent.h \
 	../lib/event.h
 
@@ -72,6 +73,7 @@ error-record.o:		error-record.c $(DEPS)
 local-agent.o:		local-agent.c $(DEPS)
 group-agent.o:		group-agent.c $(DEPS)
 simulator-agent.o:	simulator-agent.cc $(DEPS)
+console-agent.o:	console-agent.cc $(DEPS)
 node-agent.o:		node-agent.cc $(DEPS)
 event-sched_rpc.o:	event-sched.c $(DEPS)
 	$(CC) $(CFLAGS) -DRPC -c -o $@ $<
diff --git a/event/sched/console-agent.cc b/event/sched/console-agent.cc
new file mode 100644
index 0000000000..52450a21ac
--- /dev/null
+++ b/event/sched/console-agent.cc
@@ -0,0 +1,235 @@
+/*
+ * EMULAB-COPYRIGHT
+ * Copyright (c) 2004, 2005 University of Utah and the Flux Group.
+ * All rights reserved.
+ */
+
+#include "config.h"
+
+#include <errno.h>
+#include <fcntl.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/param.h>
+
+#include "console-agent.h"
+
+#ifndef min
+#define min(x, y) ((x) < (y)) ? (x) : (y)
+#endif
+
+/**
+ * A "looper" function for console agents that dequeues and processes events
+ * for a particular console.  This function will be passed to pthread_create
+ * when a new thread needs to be created to handle events.
+ *
+ * @param arg The console agent object to handle events for.
+ * @return NULL
+ *
+ * @see reload_with
+ * @see do_reboot
+ * @see local_agent_queue
+ * @see local_agent_dequeue
+ */
+static void *console_agent_looper(void *arg);
+
+console_agent_t create_console_agent(void)
+{
+	console_agent_t ca, retval;
+
+	if ((ca = (console_agent_t)
+	     malloc(sizeof(struct _console_agent))) == NULL) {
+		retval = NULL;
+		errno = ENOMEM;
+	}
+	else if (local_agent_init(&ca->ca_local_agent) != 0) {
+		retval = NULL;
+	}
+	else {
+		ca->ca_local_agent.la_flags |= LAF_MULTIPLE;
+		ca->ca_local_agent.la_looper = console_agent_looper;
+		ca->ca_mark = -1;
+		retval = ca;
+		ca = NULL;
+	}
+
+	free(ca);
+	ca = NULL;
+
+	return retval;
+}
+
+int console_agent_invariant(console_agent_t ca)
+{
+	assert(ca != NULL);
+	assert(local_agent_invariant(&ca->ca_local_agent));
+}
+
+static void do_start(console_agent_t ca, sched_event_t *se)
+{
+	int lpc;
+	
+	assert(ca != NULL);
+	assert(console_agent_invariant(ca));
+	assert(se != NULL);
+
+	for (lpc = 0; lpc < se->length; lpc++) {
+		char path[MAXPATHLEN];
+		struct agent *agent;
+		struct stat st;
+
+		if (se->length == 1)
+			agent = se->agent.s;
+		else
+			agent = se->agent.m[lpc];
+		
+		snprintf(path, sizeof(path),
+			 "%s/%s.run",
+			 TIPLOGDIR, agent->nodeid);
+		if (stat(path, &st) < 0) {
+			error("could not stat %s\n", path);
+		}
+		else {
+			ca = (console_agent_t)agent->handler;
+			ca->ca_mark = st.st_size;
+		}
+	}
+}
+
+static void do_stop(console_agent_t ca, sched_event_t *se, char *args)
+{
+	char *filename;
+	int rc, lpc;
+	
+	assert(ca != NULL);
+	assert(console_agent_invariant(ca));
+	assert(se != NULL);
+	assert(args != NULL);
+
+	if (ca->ca_mark == -1) {
+		error("CONSOLE STOP event without a START");
+		return;
+	}
+
+	if ((rc = event_arg_get(args, "FILE", &filename)) < 0) {
+		error("no filename given in CONSOLE STOP event");
+		return;
+	}
+	filename[rc] = '\0';
+	
+	for (lpc = 0; lpc < se->length; lpc++) {
+		char path[MAXPATHLEN], outpath[MAXPATHLEN];
+		int infd = -1, outfd = -1;
+		struct agent *agent;
+		size_t end_mark;
+		struct stat st;
+		
+		if (se->length == 1)
+			agent = se->agent.s;
+		else
+			agent = se->agent.m[lpc];
+		
+		snprintf(path, sizeof(path),
+			 "%s/%s.run",
+			 TIPLOGDIR, agent->nodeid);
+		if (stat(path, &st) < 0) {
+			error("could not stat %s\n", path);
+			ca->ca_mark = -1;
+			continue;
+		}
+
+		snprintf(outpath, sizeof(outpath),
+			 "logs/%s-%s.log",
+			 agent->name,
+			 filename);
+		
+		ca = (console_agent_t)agent->handler;
+		end_mark = st.st_size;
+		if ((infd = open(path, O_RDONLY)) < 0) {
+			error("could not open %s\n", path);
+		}
+		else if (lseek(infd, ca->ca_mark, SEEK_SET) < 0) {
+			error("could not seek to right place\n");
+		}
+		else if ((outfd = open(outpath,
+				       O_WRONLY|O_CREAT|O_TRUNC,
+				       0640)) < 0) {
+			error("could not create output file\n");
+		}
+		else {
+			size_t remaining = end_mark - ca->ca_mark;
+			char buffer[4096];
+
+			while ((rc = read(infd,
+					  buffer,
+					  min(remaining,
+					      sizeof(buffer)))) > 0) {
+				write(outfd, buffer, rc);
+				
+				remaining -= rc;
+			}
+		}
+		
+		if (infd != -1)
+			close(infd);
+		if (outfd != -1)
+			close(outfd);
+
+		ca->ca_mark = -1;
+	}
+}
+
+static void *console_agent_looper(void *arg)
+{
+	console_agent_t ca = (console_agent_t)arg;
+	event_handle_t handle;
+	void *retval = NULL;
+	sched_event_t se;
+
+	assert(arg != NULL);
+	
+	handle = ca->ca_local_agent.la_handle;
+
+	while (local_agent_dequeue(&ca->ca_local_agent, 0, &se) == 0) {
+		char evtype[TBDB_FLEN_EVEVENTTYPE];
+		event_notification_t en;
+		char argsbuf[BUFSIZ];
+
+		en = se.notification;
+		
+		if (!event_notification_get_eventtype(
+			handle, en, evtype, sizeof(evtype))) {
+			error("couldn't get event type from notification %p\n",
+			      en);
+		}
+		else {
+			struct agent **agent_array, *agent_singleton[1];
+			int rc, lpc, token = ~0;
+			
+			event_notification_get_arguments(handle,
+							 en,
+							 argsbuf,
+							 sizeof(argsbuf));
+			event_notification_get_int32(handle,
+						     en,
+						     "TOKEN",
+						     (int32_t *)&token);
+			argsbuf[sizeof(argsbuf) - 1] = '\0';
+
+			if (strcmp(evtype, TBDB_EVENTTYPE_START) == 0) {
+				do_start(ca, &se);
+			}
+			else if (strcmp(evtype, TBDB_EVENTTYPE_STOP) == 0) {
+				do_stop(ca, &se, argsbuf);
+			}
+			else {
+				error("cannot handle CONSOLE event %s.",
+				      evtype);
+				rc = -1;
+			}
+		}
+		sched_event_free(handle, &se);
+	}
+
+	return retval;
+}
diff --git a/event/sched/console-agent.h b/event/sched/console-agent.h
new file mode 100644
index 0000000000..0fcc9ba9bc
--- /dev/null
+++ b/event/sched/console-agent.h
@@ -0,0 +1,57 @@
+/*
+ * EMULAB-COPYRIGHT
+ * Copyright (c) 2005 University of Utah and the Flux Group.
+ * All rights reserved.
+ */
+
+/**
+ * @file console-agent.h
+ */
+
+#ifndef _console_agent_h
+#define _console_agent_h
+
+#include "event-sched.h"
+#include "local-agent.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define TIPLOGDIR "/var/log/tiplogs"
+
+/**
+ * A local agent structure for Console objects.
+ */
+struct _console_agent {
+	struct _local_agent ca_local_agent;	/*< Local agent base. */
+	off_t ca_mark;
+};
+
+/**
+ * Pointer type for the _console_agent structure.
+ */
+typedef struct _console_agent *console_agent_t;
+
+/**
+ * Create a console agent and intialize it with the default values.
+ *
+ * @return An initialized console agent object.
+ */
+console_agent_t create_console_agent(void);
+
+/**
+ * Check a console agent object against the following invariants:
+ *
+ * @li na_local_agent is sane
+ *
+ * @param na An initialized console agent object.
+ * @return True.
+ */
+int console_agent_invariant(console_agent_t na);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/event/sched/event-sched.c b/event/sched/event-sched.c
index 0081480e70..97725fec25 100644
--- a/event/sched/event-sched.c
+++ b/event/sched/event-sched.c
@@ -44,6 +44,7 @@
 #include "simulator-agent.h"
 #include "group-agent.h"
 #include "node-agent.h"
+#include "console-agent.h"
 #include "timeline-agent.h"
 
 #define EVENT_SCHED_PATH_ENV \
@@ -141,6 +142,9 @@ main(int argc, char *argv[])
 	lnNewList(&sequences);
 	lnNewList(&groups);
 
+	if ((primary_simulator_agent = create_simulator_agent()) == NULL)
+		fatal("cannot allocate simulator agent");
+
 	while ((c = getopt(argc, argv, "hVrs:p:dl:k:")) != -1) {
 		switch (c) {
 		case 'h':
@@ -449,6 +453,7 @@ int sends_complete(struct agent *agent, const char *evtype)
 		{ TBDB_OBJECTTYPE_NODE, node_completes },
 		{ TBDB_OBJECTTYPE_TIMELINE, run_completes },
 		{ TBDB_OBJECTTYPE_SEQUENCE, run_completes },
+		{ TBDB_OBJECTTYPE_CONSOLE, NULL },
 		{ NULL, NULL }
 	};
 
@@ -766,6 +771,9 @@ AddUserEnv(char *name, char *path)
 			if ((idx = strchr(buf, '\n')) != NULL)
 				*idx = '\0';
 			if ((idx = strchr(buf, '=')) != NULL) {
+				add_report_data(primary_simulator_agent,
+						SA_RDK_CONFIG,
+						buf);
 				*idx = '\0';
 				retval = setenv(strdup(buf), idx + 1, 1);
 			}
@@ -822,15 +830,10 @@ AddAgent(event_handle_t handle,
 	}
 	
 	if (strcmp(type, TBDB_OBJECTTYPE_SIMULATOR) == 0) {
-		if ((primary_simulator_agent =
-		     create_simulator_agent()) != NULL) {
-			primary_simulator_agent->sa_local_agent.la_link.
-				ln_Name = agentp->name;
-			primary_simulator_agent->sa_local_agent.la_agent =
-			    agentp;
-			agentp->handler = &primary_simulator_agent->
-				sa_local_agent;
-		}
+		primary_simulator_agent->sa_local_agent.la_link.ln_Name =
+			agentp->name;
+		primary_simulator_agent->sa_local_agent.la_agent =agentp;
+		agentp->handler = &primary_simulator_agent->sa_local_agent;
 	}
 	else if ((strcmp(type, TBDB_OBJECTTYPE_TIMELINE) == 0) ||
 		 (strcmp(type, TBDB_OBJECTTYPE_SEQUENCE) == 0)) {
@@ -868,6 +871,16 @@ AddAgent(event_handle_t handle,
 			agentp->handler = &na->na_local_agent;
 		}
 	}
+	else if (strcmp(type, TBDB_OBJECTTYPE_CONSOLE) == 0) {
+		console_agent_t ca;
+		
+		if ((ca = create_console_agent()) == NULL) {
+		}
+		else {
+			ca->ca_local_agent.la_agent = agentp;
+			agentp->handler = &ca->ca_local_agent;
+		}
+	}
 
 	if (agentp->handler != NULL) {
 		agentp->handler->la_handle = handle;
diff --git a/event/sched/node-agent.cc b/event/sched/node-agent.cc
index c752d668db..015b126c4f 100644
--- a/event/sched/node-agent.cc
+++ b/event/sched/node-agent.cc
@@ -229,7 +229,7 @@ static int do_reboot(node_agent_t na, char *nodeids)
 		warning("failed to sync log hole for node %s\n", nodeids);
 	}
 
-	printf("rebooting; %s\n", nodeids);
+	info("rebooting; %s\n", nodeids);
 
 	/* ... start the reboot. */
 	if ((retval = RPC_invoke("node.reboot",
@@ -258,21 +258,17 @@ static int do_snapshot(node_agent_t na, char *nodeids, char *args)
 
 	handle = na->na_local_agent.la_handle;
 
-	/*
-	 * Get any logs off the node(s) before we destroy them with the disk
-	 * reload, then
-	 */
 	if (systemf("loghole --port=%d --quiet sync %s",
 		    DEFAULT_RPC_PORT,
 		    nodeids) != 0) {
 		warning("failed to sync log hole for node %s\n", nodeids);
 	}
 
-	/* ... reload the default image, or */
 	if ((rc = event_arg_get(args, "IMAGE", &image_name)) < 0) {
 		warning("no image name given: %s\n", nodeids);
+
+		retval = -1;
 	}
-	/* ... a user-specified image. */
 	else {
 		image_name[rc] = '\0';
 		if ((retval = RPC_invoke("node.create_image",
diff --git a/event/sched/simulator-agent.cc b/event/sched/simulator-agent.cc
index 488379ee95..b528b6d7d5 100644
--- a/event/sched/simulator-agent.cc
+++ b/event/sched/simulator-agent.cc
@@ -115,9 +115,9 @@ int simulator_agent_invariant(simulator_agent_t sa)
 	return 1;
 }
 
-static int add_report_data(simulator_agent_t sa,
-			   sa_report_data_kind_t rdk,
-			   char *data)
+int add_report_data(simulator_agent_t sa,
+		    sa_report_data_kind_t rdk,
+		    char *data)
 {
 	char *new_data;
 	int retval;
@@ -228,7 +228,8 @@ static int do_modify(simulator_agent_t sa, int token, char *args)
 
 static void dump_report_data(FILE *file,
 			     simulator_agent_t sa,
-			     sa_report_data_kind_t srdk)
+			     sa_report_data_kind_t srdk,
+			     int clear)
 {
 	assert(file != NULL);
 	assert(sa != NULL);
@@ -272,7 +273,8 @@ static int send_report(simulator_agent_t sa, char *args)
 		error("failed to sync log holes\n");
 	}
 	
-	if ((file = popenf("mail -s \"%s: %s experiment report\" %s",
+	if ((file = popenf("tee logs/report.mail | "
+			   "mail -s \"%s: %s experiment report\" %s",
 			   "w",
 			   OURDOMAIN,
 			   pideid,
@@ -288,7 +290,7 @@ static int send_report(simulator_agent_t sa, char *args)
 		retval = 0;
 
 		/* Dump user supplied stuff first, */
-		dump_report_data(file, sa, SA_RDK_MESSAGE);
+		dump_report_data(file, sa, SA_RDK_MESSAGE, 1);
 
 		/* ... run the user-specified log digester, then */
 		if ((rc = event_arg_get(args, "DIGESTER", &digester)) > 0) {
@@ -331,8 +333,10 @@ static int send_report(simulator_agent_t sa, char *args)
 
 			fprintf(file, "loghole-archive: %s\n\n", buf);
 		}
+
+		dump_report_data(file, sa, SA_RDK_CONFIG, 0);
 		
-		dump_report_data(file, sa, SA_RDK_LOG);
+		dump_report_data(file, sa, SA_RDK_LOG, 1);
 		
 		/* ... dump the error records. */
 		if (dump_error_records(&error_records, file) != 0) {
@@ -352,6 +356,21 @@ static int send_report(simulator_agent_t sa, char *args)
 	return retval;
 }
 
+static int do_reset(simulator_agent_t sa, char *args)
+{
+	int retval = 0;
+
+	assert(sa != NULL);
+	assert(args != NULL);
+
+	if (systemf("loghole --port=%d --quiet clean",
+		    DEFAULT_RPC_PORT) != 0) {
+		error("failed to clean log holes\n");
+	}
+
+	return retval;
+}
+
 static void *simulator_agent_looper(void *arg)
 {
 	simulator_agent_t sa = (simulator_agent_t)arg;
@@ -419,6 +438,9 @@ static void *simulator_agent_looper(void *arg)
 			else if (strcmp(evtype, TBDB_EVENTTYPE_REPORT) == 0) {
 				send_report(sa, argsbuf);
 			}
+			else if (strcmp(evtype, TBDB_EVENTTYPE_RESET) == 0) {
+				do_reset(sa, argsbuf);
+			}
 			else {
 				error("cannot handle SIMULATOR event %s.",
 				      evtype);
diff --git a/event/sched/simulator-agent.h b/event/sched/simulator-agent.h
index 279322fc05..d3942cf6f4 100644
--- a/event/sched/simulator-agent.h
+++ b/event/sched/simulator-agent.h
@@ -30,6 +30,7 @@ typedef enum {
 			 the experiment. */
 	SA_RDK_LOG,	/*< Log data for the tail of the report, used for
 			  machine generated data mostly. */
+	SA_RDK_CONFIG,	/*< Config data for the tail of the report. */
 
 	SA_RDK_MAX	/*< The maximum number of message types. */
 } sa_report_data_kind_t;
@@ -78,6 +79,10 @@ simulator_agent_t create_simulator_agent(void);
  */
 int simulator_agent_invariant(simulator_agent_t sa);
 
+int add_report_data(simulator_agent_t sa,
+		    sa_report_data_kind_t rdk,
+		    char *data);
+
 #ifdef __cplusplus
 }
 #endif
diff --git a/lib/libtb/tbdefs.h b/lib/libtb/tbdefs.h
index c0305089ed..56c0a28a71 100644
--- a/lib/libtb/tbdefs.h
+++ b/lib/libtb/tbdefs.h
@@ -44,6 +44,7 @@
 #define TBDB_OBJECTTYPE_GROUP    "GROUP"
 #define TBDB_OBJECTTYPE_TIMELINE "TIMELINE"
 #define TBDB_OBJECTTYPE_SEQUENCE "SEQUENCE"
+#define TBDB_OBJECTTYPE_CONSOLE  "CONSOLE"
 
 #define TBDB_EVENTTYPE_START	"START"
 #define TBDB_EVENTTYPE_STOP	"STOP"
diff --git a/mote/GNUmakefile.in b/mote/GNUmakefile.in
index 50df052dba..bd52eb7a6f 100644
--- a/mote/GNUmakefile.in
+++ b/mote/GNUmakefile.in
@@ -1,6 +1,6 @@
 #
 # EMULAB-COPYRIGHT
-# Copyright (c) 2004 University of Utah and the Flux Group.
+# Copyright (c) 2004, 2005 University of Utah and the Flux Group.
 # All rights reserved.
 #
 
@@ -11,7 +11,7 @@ SUBDIR		= mote
 
 include $(OBJDIR)/Makeconf
 
-BIN_SCRIPTS	= tbuisp tbsgmotepower
+BIN_SCRIPTS	= tbuisp tbsgmotepower newmote
 
 #
 # Force dependencies on the scripts so that they will be rerun through
diff --git a/mote/newmote.in b/mote/newmote.in
new file mode 100644
index 0000000000..ae45798171
--- /dev/null
+++ b/mote/newmote.in
@@ -0,0 +1,124 @@
+#!/usr/bin/perl -wT
+#
+# EMULAB-COPYRIGHT
+# Copyright (c) 2005 University of Utah and the Flux Group.
+# All rights reserved.
+#
+
+#
+# newmote -
+#
+
+use lib '@prefix@/lib';
+my $TB = '@prefix@';
+ 
+use libdb;
+use English;
+use Getopt::Std;
+
+# un-taint path
+$ENV{'PATH'} = "/bin:/usr/bin:/usr/local/bin:$TB/bin";
+delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};
+ 
+use strict;
+
+#
+# Constants
+#
+my $DEBUG  = 1;
+
+#
+# Experiments we might put nodes into
+#
+my $PID_HWDOWN = NODEDEAD_PID();
+my $EID_HWDOWN = NODEDEAD_EID();
+
+my $nalloc = "$TB/bin/nalloc";
+my $phys_nodeid = "";
+my $do_nalloc = 1;
+
+#
+# Handle command-line arguments
+# TODO: Allow a user to specify some of their own arguments to uisp
+#
+sub usage() {
+    print STDERR "Usage: $0 [-hdf] [-p phys] <type> <motes...>\n";
+    exit(2);
+}
+
+my $optlist = "dhfp:";
+
+my %options = ();
+if (! getopts($optlist, \%options)) {
+    usage();
+}
+if (defined($options{"d"})) {
+    $DEBUG = 1;
+}
+if (defined($options{"h"})) {
+    usage();
+}
+if (defined($options{"f"})) {
+    $do_nalloc = 0;
+}
+if (defined($options{"p"})) {
+    $phys_nodeid = $options{"p"};
+
+    if ($phys_nodeid =~ /^([-\w]+)$/) {
+	$phys_nodeid = $1;
+    }
+    else {
+	die("*** Bad phys_nodeid: $phys_nodeid.\n");
+    }
+}
+
+if (@ARGV < 2) {
+    usage();
+}
+
+my $type = shift(@ARGV);
+my @node_ids = @ARGV;
+
+if ($type =~ /^([-\w]+)$/) {
+    $type = $1;
+}
+else {
+    die("*** Bad mote type: $type.\n");
+}
+
+foreach my $node_id (@node_ids) {
+    if ($node_id =~ /^([-\w]+)$/) {
+	$node_id = $1;
+    }
+    else {
+	die("*** Bad node id: $node_id.\n");
+    }
+
+    my $pnode;
+    if ($phys_nodeid eq "") {
+	$pnode = $node_id;
+    } else {
+	$pnode = $phys_nodeid;
+    }
+    
+    DBQueryFatal("REPLACE INTO nodes SET ".
+		 "node_id='$node_id',type='$type',phys_nodeid='$pnode',".
+		 "role='testnode',def_boot_osid='emulab-ops-TinyOS-STD',".
+		 "bootstatus='okay',status='up',status_timestamp=NOW(),".
+		 "failureaction='fatal',routertype='none',eventstate='ISUP',".
+		 "state_timestamp=NOW(),op_mode='ALWAYSUP',".
+		 "op_mode_timestamp=NOW(),allocstate='FREE',".
+		 "allocstate_timestamp=NOW()");
+
+    DBQueryFatal("REPLACE INTO partitions SET ".
+		 "node_id='$node_id',partition=1,".
+		 "osid='emulab-ops-TinyOS-STD'");
+    
+    DBQueryFatal("REPLACE INTO tiplines SET ".
+		 "tipname='$node_id',node_id='$node_id',".
+		 "server='@USERNODE@'"); # XXX Better subst for user node
+
+    if ($do_nalloc) {
+	system "$nalloc $PID_HWDOWN $EID_HWDOWN $node_id";
+    }
+}
diff --git a/mote/tbuisp.in b/mote/tbuisp.in
index a9c2ba7a0f..9bee638526 100755
--- a/mote/tbuisp.in
+++ b/mote/tbuisp.in
@@ -37,6 +37,8 @@ my $UISP   = "$TB/bin/uisp";
 my $SGUISP = "/usr/local/bin/uisp";
 my $SSHTB  = "$TB/bin/sshtb";
 my $POWER  = "$TB/bin/power";
+my $TIP    = "$TB/bin/tiptunnel";
+my $USERS  = "@USERNODE@";
 my $DEBUG  = 1;
 
 #
@@ -188,8 +190,15 @@ MOTE: foreach my $mote (@motes) {
 	next MOTE;
     }
     if ($host eq $mote) {
-	warn "Error - no host found for $mote - skipping\n";
-	$errors++;
+	print "Uploading code to $mote\n";
+	my $commandstr = "$SSHTB -host $USERS $TIP -u $UID -l $mote - < $filename";
+	my $OLDUID = $UID;
+	$UID = $EUID;
+	if (system($commandstr)) {
+	    $errors++;
+	    warn "Failed to upload code to $mote";
+	}
+	$UID = $OLDUID;
 	next MOTE;
     }
     my ($hosttype, $hostclass) = TBNodeType($host);
diff --git a/sql/database-fill.sql b/sql/database-fill.sql
index 672017a928..fb38194929 100644
--- a/sql/database-fill.sql
+++ b/sql/database-fill.sql
@@ -243,10 +243,10 @@ REPLACE INTO mode_transitions VALUES ('MINIMAL','SHUTDOWN','PXEFBSD','SHUTDOWN',
 REPLACE INTO mode_transitions VALUES ('NORMAL','REBOOTING','NORMALv2','SHUTDOWN','');
 REPLACE INTO mode_transitions VALUES ('NORMALv2','SHUTDOWN','NORMAL','REBOOTING','');
 REPLACE INTO mode_transitions VALUES ('NORMALv1','SHUTDOWN','NORMALv2','SHUTDOWN','');
-REPLACE INTO mode_transitions VALUES ('RELOAD-MOTE','SHUTDOWN','ALWAYSUP','SHUTDOWN','ReloadDone');
 REPLACE INTO mode_transitions VALUES ('ALWAYSUP','SHUTDOWN','RELOAD-MOTE','SHUTDOWN','ReloadStart');
 REPLACE INTO mode_transitions VALUES ('ALWAYSUP','ISUP','RELOAD-MOTE','SHUTDOWN','ReloadStart');
 REPLACE INTO mode_transitions VALUES ('ALWAYSUP','ISUP','RELOAD-MOTE','ISUP','ReloadStart');
+REPLACE INTO mode_transitions VALUES ('RELOAD-MOTE','SHUTDOWN','ALWAYSUP','ISUP','ReloadDone');
 
 --
 -- Dumping data for table `state_timeouts`
diff --git a/tbsetup/ns2ir/GNUmakefile.in b/tbsetup/ns2ir/GNUmakefile.in
index 139aca84a7..8f5041f07d 100644
--- a/tbsetup/ns2ir/GNUmakefile.in
+++ b/tbsetup/ns2ir/GNUmakefile.in
@@ -17,7 +17,8 @@ include $(OBJDIR)/Makeconf
 LIB_STUFF    = lanlink.tcl node.tcl sim.tcl tb_compat.tcl null.tcl \
 		  nsobject.tcl traffic.tcl vtype.tcl parse.tcl program.tcl \
 		  nsenode.tcl nstb_compat.tcl event.tcl firewall.tcl \
-		  elabinelab.ns fw.ns timeline.tcl sequence.tcl topography.tcl
+		  elabinelab.ns fw.ns timeline.tcl sequence.tcl \
+		  topography.tcl console.tcl
 BOSSLIBEXEC  = parse-ns
 USERLIBEXEC  = parse.proxy
 
diff --git a/tbsetup/ns2ir/console.tcl b/tbsetup/ns2ir/console.tcl
new file mode 100644
index 0000000000..1b9f9d730f
--- /dev/null
+++ b/tbsetup/ns2ir/console.tcl
@@ -0,0 +1,49 @@
+# -*- tcl -*-
+#
+# EMULAB-COPYRIGHT
+# Copyright (c) 2000-2005 University of Utah and the Flux Group.
+# All rights reserved.
+#
+
+######################################################################
+# console.tcl
+#
+# This defines the console agent.
+#
+######################################################################
+
+Class Console -superclass NSObject
+
+namespace eval GLOBALS {
+    set new_classes(Console) {}
+}
+
+Console instproc init {s n} {
+    $self set sim $s
+    $self set node $n
+    $self set connected 0
+}
+
+Console instproc rename {old new} {
+    $self instvar sim
+
+    $sim rename_console $old $new
+}
+
+# updatedb DB
+# This adds rows to the virt_trafgens table corresponding to this agent.
+Console instproc updatedb {DB} {
+    var_import ::GLOBALS::pid
+    var_import ::GLOBALS::eid
+    var_import ::TBCOMPAT::objtypes
+    $self instvar node
+    $self instvar sim
+
+    if {$node == {}} {
+	perror "\[updatedb] $self has no node."
+	return
+    }
+
+    # Update the DB
+    $sim spitxml_data "virt_agents" [list "vnode" "vname" "objecttype"] [list $node $self $objtypes(CONSOLE)]
+}
diff --git a/tbsetup/ns2ir/node.tcl b/tbsetup/ns2ir/node.tcl
index 99addd0219..e7d9cd6646 100644
--- a/tbsetup/ns2ir/node.tcl
+++ b/tbsetup/ns2ir/node.tcl
@@ -86,6 +86,11 @@ Node instproc init {s} {
     $self set Z_ 0.0
     $self set orientation_ 0.0
 
+    set cname "${self}-console"
+    Console $cname $s $self
+    $s add_console $cname
+    $self set console_ $cname
+
     if { ${::GLOBALS::simulated} == 1 } {
 	$self set simulated 1
     } else {
@@ -97,10 +102,16 @@ Node instproc init {s} {
 # The following procs support renaming (see README)
 Node instproc rename {old new} {
     $self instvar portlist
+    $self instvar console_
+
     foreach object $portlist {
 	$object rename_node $old $new
     }
     [$self set sim] rename_node $old $new
+    $console_ set node $new
+    $console_ rename "${old}-console" "${new}-console"
+    uplevel "#0" rename "${old}-console" "${new}-console"
+    set console_ ${new}-console
 }
 
 Node instproc rename_lanlink {old new} {
@@ -251,7 +262,7 @@ Node instproc updatedb {DB} {
 	    return
 	}
 
-	if {! [$topo checkdest $self $X_ $Y_]} {
+	if {! [$topo checkdest $self $X_ $Y_ -showerror 1]} {
 	    return
 	}
 
@@ -542,3 +553,9 @@ Node instproc topography {topo} {
 	$self set type "robot"
     }
 }
+
+Node instproc console {} {
+    $self instvar console_
+    
+    return $console_
+}
diff --git a/tbsetup/ns2ir/parse.tcl.in b/tbsetup/ns2ir/parse.tcl.in
index d5725cee4d..cf85233570 100644
--- a/tbsetup/ns2ir/parse.tcl.in
+++ b/tbsetup/ns2ir/parse.tcl.in
@@ -352,6 +352,7 @@ source ${GLOBALS::libdir}/event.tcl
 source ${GLOBALS::libdir}/firewall.tcl
 source ${GLOBALS::libdir}/timeline.tcl
 source ${GLOBALS::libdir}/sequence.tcl
+source ${GLOBALS::libdir}/console.tcl
 source ${GLOBALS::libdir}/topography.tcl
 
 ##################################################
diff --git a/tbsetup/ns2ir/sequence.tcl b/tbsetup/ns2ir/sequence.tcl
index 324afbf9f0..9148ffa267 100644
--- a/tbsetup/ns2ir/sequence.tcl
+++ b/tbsetup/ns2ir/sequence.tcl
@@ -43,6 +43,16 @@ EventSequence instproc init {s seq args} {
     set ::GLOBALS::last_class $self
 }
 
+EventSequence instproc append {event} {
+    $self instvar sim
+    $self instvar event_list
+    
+    set rc [$sim make_event "sequence" $event]
+    if {$rc != {}} {
+	lappend event_list $rc
+    }
+}
+
 EventSequence instproc rename {old new} {
     $self instvar sim
 
diff --git a/tbsetup/ns2ir/sim.tcl.in b/tbsetup/ns2ir/sim.tcl.in
index c4c6db52d2..ab9426e489 100644
--- a/tbsetup/ns2ir/sim.tcl.in
+++ b/tbsetup/ns2ir/sim.tcl.in
@@ -78,6 +78,12 @@ Simulator instproc init {args} {
     $self instvar sequence_list;
     array set sequence_list {}
 
+    $self instvar console_list;
+    array set console_list {}
+
+    $self instvar tiptunnel_list;
+    set tiptunnel_list {}
+
     var_import ::GLOBALS::last_class
     set last_class $self
 
@@ -319,6 +325,8 @@ Simulator instproc run {} {
     $self instvar firewall_list
     $self instvar timeline_list
     $self instvar sequence_list
+    $self instvar console_list
+    $self instvar tiptunnel_list
     $self instvar simulated
     $self instvar nseconfig
     var_import ::GLOBALS::pid
@@ -502,6 +510,12 @@ Simulator instproc run {} {
     foreach seq [array names sequence_list] {
 	$seq updatedb "sql"
     }
+    foreach con [array names console_list] {
+	$con updatedb "sql"
+    }
+    foreach tt $tiptunnel_list {
+	$self spitxml_data "virt_tiptunnels" [list "host" "vnode"] $tt
+    }
 
     set fields [list "mem_usage" "cpu_usage" "forcelinkdelays" "uselinkdelays" "usewatunnels" "uselatestwadata" "wa_delay_solverweight" "wa_bw_solverweight" "wa_plr_solverweight" "veth_encapsulate" "allowfixnode"]
     set values [list $mem_usage $cpu_usage $forcelinkdelays $uselinkdelays $usewatunnels $uselatestwadata $wa_delay_solverweight $wa_bw_solverweight $wa_plr_solverweight $veth_encapsulate $fix_current_resources]
@@ -616,6 +630,25 @@ Simulator instproc attach-agent {node agent} {
 # connect <src> <dst>
 # Connects two agents together.
 Simulator instproc connect {src dst} {
+    $self instvar tiptunnel_list
+
+    if {([$src info class Node] && [$dst info class Console]) ||
+	([$src info class Console] && [$dst info class Node])} {
+	if {[$src info class Node] && [$dst info class Console]} {
+	    set node $src
+	    set con $dst
+	} else {
+	    set node $dst
+	    set con $src
+	}
+	if {[$con set connected]} {
+	    perror "\[connect] $con is already connected"
+	    return
+	}
+	$con set connected 1
+	lappend tiptunnel_list [list $node [$con set node]]
+	return
+    }
     set error 0
     if {! [$src info class Agent]} {
 	perror "\[connect] $src is not an Agent."
@@ -784,6 +817,12 @@ Simulator instproc rename_sequence {old new} {
     set sequence_list($new) {}
 }
 
+Simulator instproc rename_console {old new} {
+    $self instvar console_list
+    unset console_list($old)
+    set console_list($new) {}
+}
+
 # find_link <node1> <node2>
 # This is just an accesor to the link_map datastructure.  If no
 # link is known between <node1> and <node2> the empty list is returned.
@@ -946,6 +985,13 @@ Simulator instproc add_eventgroup {group} {
     set eventgroup_list($group) {}
 }
 
+# add_console
+# Link to a Console object.
+Simulator instproc add_console {console} {
+    $self instvar console_list
+    set console_list($console) {}
+}
+
 # add_firewall
 # Link to a Firewall object.
 Simulator instproc add_firewall {fw} {
@@ -1176,12 +1222,12 @@ Simulator instproc make_event {outer event} {
 		    }
 		    set x [lindex $evargs 0]
 		    set y [lindex $evargs 1]
-		    if {! [$topo checkdest $self $x $y]} {
+		    if {! [$topo checkdest $self $x $y -showerror 1]} {
 			return
 		    }
 		    set speed [lindex $evargs 2]
-		    if {$speed != 0.0 && $speed != 0.1} {
-			perror "Speed is currently locked at 0.0 or 0.1"
+		    if {$speed != 0.0 && ($speed < 0.1) && ($speed > 0.4)} {
+			perror "Speed is currently locked at 0.0 or 0.1-0.4"
 			return
 		    }
 		    ::GLOBALS::named-args [lrange $evargs 3 end] {
@@ -1257,8 +1303,16 @@ Simulator instproc make_event {outer event} {
 	    }
 	    
 	    switch -- $cmd {
+		"set" -
 		"run" {
-		    set etype RUN
+		    switch -- $cmd {
+			"set" {
+			    set etype MODIFY
+			}
+			"run" {
+			    set etype RUN
+			}
+		    }
 		    if {[$obj info class] == "EventGroup"} {
 			set default_command {}
 		    } else {
@@ -1317,6 +1371,25 @@ Simulator instproc make_event {outer event} {
 		}
 	    }
 	}
+	"Console" {
+	    set otype CONSOLE
+	    set vname $obj
+
+	    switch -- $cmd {
+		"start" {
+		    set etype START
+		}
+		"stop" {
+		    set etype STOP
+		    if {[llength $event] < 3} {
+			perror "Wrong number of arguments: $obj $cmd $evargs"
+			return
+		    }
+		    set arg [lindex $event 2]
+		    set args "FILE=$arg"
+		}
+	    }
+	}
 	"Simulator" {
 	    set vnode "*"
 	    set vname $self
@@ -1377,6 +1450,11 @@ Simulator instproc make_event {outer event} {
 			set args "DIGESTER={$(-digester)}"
 		    }
 		}
+		"cleanlogs" {
+		    set otype SIMULATOR
+		    set etype RESET
+		    set args "ASPECT=LOGHOLE"
+		}
 		unknown {
 		    punsup "$obj $cmd $evargs"
 		    return
diff --git a/tbsetup/ns2ir/topography.tcl b/tbsetup/ns2ir/topography.tcl
index 638431ba6a..01d85003ef 100644
--- a/tbsetup/ns2ir/topography.tcl
+++ b/tbsetup/ns2ir/topography.tcl
@@ -46,8 +46,8 @@ Topography instproc load_area {area} {
 
     $self set area_name $area
     # XXX Load the width/height of the floor in here.
-    $self set width 10.0
-    $self set height 10.0
+    $self set width 50.0
+    $self set height 50.0
 }
 
 Topography instproc initialized {} {
@@ -61,19 +61,27 @@ Topography instproc initialized {} {
     }
 }
 
-Topography instproc checkdest {obj x y} {
+Topography instproc checkdest {obj x y args} {
     var_import ::TBCOMPAT::obstacles
     var_import ::TBCOMPAT::cameras
     $self instvar area_name
     $self instvar width
     $self instvar height
 
+    ::GLOBALS::named-args $args {
+	-showerror 0
+    }
+
     if {$x < 0 || $x >= $width} {
-	perror "$x is out of bounds for node \"$obj\""
+	if {$(-showerror)} {
+	    perror "$x is out of bounds for node \"$obj\""
+	}
 	return 0
     }
     if {$y < 0 || $y >= $height} {
-	perror "$y is out of bounds for node \"$obj\""
+	if {$(-showerror)} {
+	    perror "$y is out of bounds for node \"$obj\""
+	}
 	return 0
     }
     
@@ -86,7 +94,9 @@ Topography instproc checkdest {obj x y} {
 	        ($x <= [expr $obstacles($id,$area_name,x2) + 0.25]) &&
 	        ($y >= [expr $obstacles($id,$area_name,y1) - 0.25]) &&
 	        ($y <= [expr $obstacles($id,$area_name,y2) + 0.25])} {
-		    perror "Destination $x,$y puts $obj in obstacle $value."
+		    if {$(-showerror)} {
+			perror "Destination $x,$y puts $obj in obstacle $value."
+		    }
 		    return 0
 	    }
 	}
@@ -107,7 +117,9 @@ Topography instproc checkdest {obj x y} {
 	}
 
 	if {$in_cam == ""} {
-	    perror "Destination $x,$y is out of view of the tracking cameras";
+	    if {$(-showerror)} {
+		perror "Destination $x,$y is out of view of the tracking cameras";
+	    }
 	    return 0
 	}
     }
diff --git a/tbsetup/tbreport.in b/tbsetup/tbreport.in
index 4a5831ea59..e7057942f3 100644
--- a/tbsetup/tbreport.in
+++ b/tbsetup/tbreport.in
@@ -758,7 +758,7 @@ if ($showevents) {
 		"left join event_eventtypes as et on ex.eventtype=et.idx ".
 		"left join event_objecttypes as ot on ex.objecttype=ot.idx ".
 		"where ex.pid='$pid' and ex.eid='$eid' ".
-		"order by time,vnode,vname,ex.idx");
+		"order by time,ex.idx,vnode,vname");
 
     if ($result->numrows) {
 	if ($verbose) {
diff --git a/tip/GNUmakefile.in b/tip/GNUmakefile.in
index 4d6a7fd680..b8f89df8ac 100644
--- a/tip/GNUmakefile.in
+++ b/tip/GNUmakefile.in
@@ -5,13 +5,16 @@ SUBDIR		= tip
 
 include $(OBJDIR)/Makeconf
 
-all:	tip tiptunnel console
+all:	tip tiptunnel console tippty
+client: tippty
 
 include $(TESTBED_SRCDIR)/GNUmakerules
 
 SSLFLAGS = -DWITHSSL
 SSLLIBS	 = -lssl -lcrypto
+PTYLIBS  =
 SYSTEM := $(shell uname -s)
+PTYLIBS += -lutil
 ifeq ($(SYSTEM),Linux)
 NEEDKERB := $(shell nm /usr/lib/libssl.a | grep -q krb; echo $$?)
 ifeq ($(NEEDKERB),0)
@@ -20,7 +23,7 @@ ifeq ($(NEEDKERB),0)
 endif
 endif
 
-CC = gcc -g -O2 -DUSESOCKETS -I$(TESTBED_SRCDIR)/capture
+CC = gcc -g -O2 -DUSESOCKETS -I$(TESTBED_SRCDIR)/capture -I$(OBJDIR)
 
 OBJS = cmds.o cmdtab.o hunt.o partab.o \
        remote.o tip.o value.o vars.o getcap.o
@@ -52,10 +55,19 @@ console.o: tiptunnel.c $(TESTBED_SRCDIR)/capture/capdecls.h
 console: console.o
 	$(CC) -o console console.o
 
+tippty.o: tiptunnel.c $(TESTBED_SRCDIR)/capture/capdecls.h
+	$(CC) -DTIPPTY -o $@ -c $<
+
+tippty: tippty.o
+	$(CC) $(PTYLIBS) -o $@ $<
+
 $(OBJS): tipconf.h tip.h
 
+client-install: client
+	$(INSTALL_PROGRAM) tippty$(EXE) $(DESTDIR)$(CLIENT_BINDIR)/tippty$(EXE)
 
 control-install tipserv-install install:	all $(INSTALL_BINDIR)/tip $(INSTALL_BINDIR)/tiptunnel $(INSTALL_BINDIR)/console
+	$(INSTALL_PROGRAM) tiptunnel $(INSTALL_DIR)/opsdir/bin/tiptunnel
 
 clean:
-	rm -f $(OBJS) tiptunnel.o console.o tip tiptunnel console
+	rm -f $(OBJS) tiptunnel.o console.o tip tiptunnel console tippty.o tippty
diff --git a/tip/tiptunnel.c b/tip/tiptunnel.c
index a9adf8a287..62295bdae3 100644
--- a/tip/tiptunnel.c
+++ b/tip/tiptunnel.c
@@ -1,8 +1,21 @@
+
+#include "config.h"
+
+#include <errno.h>
 #include <stdlib.h>
 #include <stdio.h>
 #include <string.h>
 #include <unistd.h>
+#include <fcntl.h>
+#include <grp.h>
+#include <pwd.h>
+#include <signal.h>
+#include <assert.h>
+#include <paths.h>
+
+#include <sys/time.h>
 
+#include <sys/types.h>
 #include <sys/param.h>
 #include <sys/socket.h>
 #include <netinet/in.h>
@@ -16,7 +29,9 @@
 #include "capdecls.h"
 
 int localmode = 0;
+int uploadmode = 0;
 
+#define ACLDIR "/var/log/tiplogs"
 #define DEFAULT_PROGRAM "xterm -T TIP -e telnet localhost @s"
 
 int debug = 0;
@@ -31,6 +46,9 @@ int tunnelSock = 0;
 char * programToLaunch = NULL;
 char * hostname = NULL;
 
+char * certfile = NULL;
+char * user = NULL;
+
 secretkey_t key;
 
 typedef int WriteFunc( void * data, int size );
@@ -65,16 +83,54 @@ char * certString = NULL;
 
 #endif /* WITHSSL */
 
+#if defined(TIPPTY)
+
+#if defined(__FreeBSD__)
+# include <libutil.h>
+#endif
+
+#if defined(linux)
+# if !defined(INFTIM)
+#  define INFTIM -1
+# endif
+# include <pty.h>
+# include <utmp.h>
+#endif
+
+#include <poll.h>
+#include <termios.h>
+
+#define TIPDIR "/dev/tip"
+#define POLL_HUP_INTERVAL (250) /* ms */
+
+typedef struct {
+    char data[4096];
+    size_t inuse;
+} buffer_t;
+
+static void pack_buffer(buffer_t *buffer, int amount);
+static void dotippty(char *nodename);
+
+static char pidfilename[1024] = "";
+
+static void sigquit(int sig)
+{
+  if (strlen(pidfilename) > 0)
+    unlink(pidfilename);
+  exit(0);
+}
+#endif
+
 int main( int argc, char ** argv )
 {
   const char * name = argv[0];
   int op;
 
-#ifdef LOCALBYDEFAULT
+#if defined(LOCALBYDEFAULT) || defined(TIPPTY)
   localmode++;
 #endif
 
-  while ((op = getopt( argc, argv, "hlsp:rd" )) != -1) {
+  while ((op = getopt( argc, argv, "hlsp:rdu:c:" )) != -1) {
     switch (op) {
       case 'h':
         usage(name);
@@ -91,6 +147,16 @@ int main( int argc, char ** argv )
       case 'r':
 	allowRemote++;
 	break;
+#ifdef WITHSSL
+      case 'u':
+	user = optarg;
+	uploadmode++;
+	usingSSL++;
+	break;
+      case 'c':
+	certfile = optarg;
+	break;
+#endif
     }
   }
 
@@ -109,7 +175,7 @@ int main( int argc, char ** argv )
 
   if (localmode) {
     char localAclName[1024];
-    sprintf( localAclName, "/var/log/tiplogs/%s.acl", argv[0] );
+    sprintf( localAclName, "%s/%s.acl", ACLDIR, argv[0] );
     loadAcl( localAclName );
   } else {
     loadAcl( argv[0] );
@@ -131,7 +197,76 @@ int main( int argc, char ** argv )
     doConnect();
   }
 
+#if defined(TIPPTY)
+  if (!debug) {
+    FILE *file;
+    
+    daemon(0, 0);
+    signal(SIGINT, sigquit);
+    signal(SIGTERM, sigquit);
+    signal(SIGQUIT, sigquit);
+    snprintf(pidfilename, sizeof(pidfilename),
+	     "%s/tippty.%s.pid",
+	     _PATH_VARRUN, argv[0]);
+    if ((file = fopen(pidfilename, "w")) != NULL) {
+      fprintf(file, "%d\n", getpid());
+      fclose(file);
+    }
+  }
+#endif
+
+  if (user && (getuid() == 0)) {
+    struct passwd *pw;
+    struct group *gr;
+    uid_t uid;
+    int rc;
+    
+    if ((sscanf(user, "%d", &uid) == 1 && (pw = getpwuid(uid)) == NULL) &&
+	(pw = getpwnam(user)) == NULL) {
+      fprintf(stderr, "invalid user: %s %d\n", user, uid);
+      exit(1);
+    }
+    
+    if ((gr = getgrgid(pw->pw_gid)) == NULL) {
+      fprintf(stderr, "invalid group: %d\n", pw->pw_gid);
+      exit(1);
+    }
+    
+    /*
+     * Initialize the group list, and then flip to uid.
+     */
+    if (setgid(pw->pw_gid) ||
+	initgroups(user, pw->pw_gid) ||
+	setuid(pw->pw_uid)) {
+      fprintf(stderr, "Could not become user: %s\n", user);
+      exit(1);
+    }
+  }
+
+  if (uploadmode) {
+    int fd = 0;
+
+    if ((strcmp(argv[1], "-") != 0) && (fd = open(argv[1], O_RDONLY)) < 0) {
+      fprintf(stderr, "Cannot open file: %s\n", argv[1]);
+      exit(1);
+    }
+    else {
+      char buf[4096];
+      int rc;
+
+      while ((rc = read(fd, buf, sizeof(buf))) > 0) {
+	writeFunc(buf, rc);
+      }
+      close(fd);
+    }
+    exit(0);
+  }
+  
   doAuthenticate();
+
+#if defined(TIPPTY)
+  dotippty(argv[0]);
+#else
   doCreateTunnel();
 
   if (programToLaunch) {
@@ -179,11 +314,12 @@ int main( int argc, char ** argv )
   //if (localmode) { sleep(3); }
   doTunnelConnection();
   if (debug) { printf("tiptunnel closing.\n"); }
+#endif
 }
 
 void usage(const char * name)
 {
-#ifdef LOCALBYDEFAULT
+#if defined(LOCALBYDEFAULT)
 
   printf("Usage:\n"
 	 "%s [-d] <pcname>\n"
@@ -192,6 +328,12 @@ void usage(const char * name)
 	 name
 	 );
 
+#elif defined(TIPPTY)
+
+  printf("Usage:\n"
+	 "%s [-d] <node>\n",
+	 name);
+  
 #else
 
   printf("No aclfile specified.\n"
@@ -208,6 +350,7 @@ void usage(const char * name)
 	 "-p <portnum>     specifies tunnel port number\n"
 	 "-d               turns on more verbose debug messages\n"
 	 "-r               allows connections to tunnel from non-localhost\n"
+	 "-u <user>        upload mode\n"
 	 "\n"
 	 "<program>        (non-local-mode only)\n"
 	 "                 path of program to launch; default is\n"
@@ -240,14 +383,22 @@ void loadAcl( const char * filename )
 
   while (fscanf(aclFile, "%s %s\n", &b1, &b2) != EOF) {
     if ( strcmp(b1, "host:") == 0 ) {
-      hostname = strdup( b2 );
+      if (!uploadmode)
+	hostname = strdup( b2 );
     } else if ( strcmp(b1, "port:") == 0 ) {
-      port = atoi( b2 );
+      if (!uploadmode)
+	port = atoi( b2 );
     } else if ( strcmp(b1, "keylen:") == 0 ) {
       key.keylen = atoi( b2 );
     } else if ( strcmp(b1, "key:") == 0 ) {
       strcpy( key.key, b2 );
 #ifdef WITHSSL
+    } else if ( strcmp(b1, "uphost:") == 0 ) {
+      if (uploadmode)
+	hostname = strdup( b2 );
+    } else if ( strcmp(b1, "upport:") == 0 ) {
+      if (uploadmode)
+	port = atoi( b2 );
     } else if ( strcmp(b1, "ssl-server-cert:") == 0 ) {
       if (debug) { printf("Using SSL to connect to capture.\n"); }
       certString = strdup( b2 );
@@ -520,12 +671,27 @@ acceptor:
 
 #ifdef WITHSSL
 
+#define DEFAULT_CERTFILE TBROOT"/etc/capture.pem"
+
 void initSSL()
 {
   SSL_library_init();
   ctx = SSL_CTX_new( SSLv23_method() );
   SSL_load_error_strings();
-  
+
+  if (uploadmode) {
+    if (!certfile) { certfile = DEFAULT_CERTFILE; }
+    
+    if (SSL_CTX_use_certificate_file( ctx, certfile, SSL_FILETYPE_PEM ) <= 0) {
+      fprintf( stderr, "Could not load %s as certificate file.", certfile );
+      exit(1);
+    }
+    
+    if (SSL_CTX_use_PrivateKey_file( ctx, certfile, SSL_FILETYPE_PEM ) <= 0) {
+      fprintf( stderr, "Could not load %s as key file.", certfile );
+      exit(1);
+    }
+  }
   //  if (!(SSL_CTX_load_verify_location( ctx, CA_LIST, 0 ))
 }
 
@@ -544,7 +710,10 @@ void sslConnect()
     char ret[4];
 
     sslHintKey.keylen = 7;
-    strncpy( sslHintKey.key, "USESSL", 7 );
+    if (uploadmode)
+      strcpy( sslHintKey.key, "UPLOAD" );
+    else
+      strncpy( sslHintKey.key, "USESSL", 7 );
     write( sock, &sslHintKey, sizeof( sslHintKey ) );
     /*
     if (4 != read( sock, ret, 4 ) || 
@@ -553,7 +722,7 @@ void sslConnect()
       exit(-1);
     }
     */
-  } 
+  }
 
   ssl = SSL_new( ctx );
   SSL_set_fd( ssl, sock );
@@ -570,7 +739,7 @@ void sslConnect()
 
   // sbio = BIO_new_socket( sock, BIO_NOCLOSE );
   // SSL_set_bio( ssl, sbio, sbio );
-  sleep(1);
+  // sleep(1);
   
   if (SSL_connect( ssl ) <= 0) {
     fprintf(stderr, "SSL Connect error.\n");
@@ -578,6 +747,9 @@ void sslConnect()
     exit(-1);
   }
 
+  if (uploadmode)
+    return;
+
   peer = SSL_get_peer_certificate( ssl );
 
   // X509_print_fp( stdout );
@@ -587,6 +759,8 @@ void sslConnect()
   
   X509_digest( peer, EVP_sha(), digest, &len );
 
+  X509_free( peer );
+
   for (i = 0; i < len; i++) {
     sprintf( digestHex + (i * 2), "%02x", (unsigned int) digest[i] );
   }
@@ -623,3 +797,150 @@ int readSSL( void * data, int size )
 }
 
 #endif /* WITHSSL */
+
+#ifdef TIPPTY
+
+static void pack_buffer(buffer_t *buffer, int amount)
+{
+    assert(buffer != NULL);
+    assert((amount == -1) ||
+	   ((amount >= 0) && (amount < sizeof(buffer->data))));
+    
+    if (amount > 0) {
+	buffer->inuse -= amount;
+	memmove(buffer->data, &buffer->data[amount], buffer->inuse);
+    }
+}
+
+static void dotippty(char *nodename)
+{
+  int rc, master, slave;
+  char path[64];
+
+  assert(nodename != NULL);
+  assert(strlen(nodename) > 0);
+  
+  if ((mkdir(TIPDIR, 0755) < 0) && (errno != EEXIST))
+    perror("unable to make " TIPDIR);
+  
+  if ((rc = openpty(&master, &slave, path, NULL, NULL)) < 0) {
+    perror("openpty");
+    exit(1);
+  }
+  else {
+    struct pollfd pf[2] = {
+      [0] = { .fd = sock, .events = POLLIN },
+      [1] = { .fd = master, .events = POLLHUP },
+    };
+    
+    buffer_t from_pty = { .inuse = 0 }, to_pty = { .inuse = 0 };
+    int tweaked = 0, fd_count = 2;
+    struct timeval now, before;
+    char linkpath[128];
+    
+    fcntl(master, F_SETFL, O_NONBLOCK);
+    fcntl(sock, F_SETFL, O_NONBLOCK);
+    close(slave);
+    slave = -1;
+
+    if (chmod(path, 0666) < 0)
+      perror("unable to change permissions");
+
+    snprintf(linkpath, sizeof(linkpath), "%s/%s", TIPDIR, nodename);
+    if ((symlink(path, linkpath) < 0) && (errno != EEXIST))
+      perror("unable to create symlink");
+
+    gettimeofday(&now, NULL);
+    before = now;
+    while ((rc = poll(pf,
+		      fd_count,
+		      fd_count == 1 ? POLL_HUP_INTERVAL : INFTIM)) >= 0) {
+      if (rc == 0) { // Timeout
+	/* ... check for a slave connection on the next loop. */
+	fd_count = 2;
+      }
+      else if (pf[1].revents & POLLHUP) {
+	struct timeval diff;
+	
+	/* No slave connection. */
+	if (pf[0].revents & POLLIN) // Drain the input side.
+	  rc = readFunc(to_pty.data, sizeof(to_pty.data));
+	if (rc < 0)
+	  exit(0);
+	if ((pf[0].revents & POLLOUT) && from_pty.inuse > 0) {
+	  // Drain our buffer to the output.
+	  rc = writeFunc(from_pty.data, from_pty.inuse);
+	  if (rc < 0)
+	    exit(0);
+	  pack_buffer(&from_pty, rc);
+	}
+	to_pty.inuse = 0; // Nowhere to send.
+	gettimeofday(&now, NULL);
+	timersub(&now, &before, &diff);
+	if (diff.tv_sec > 0 || diff.tv_usec > (POLL_HUP_INTERVAL * 100)) {
+	  before = now;
+	  fd_count = 2;
+	}
+	else {
+	  fd_count = 1; // Don't poll the master and turn on timeout.
+	}
+	tweaked = 0;
+      }
+      else {
+	if (!tweaked) {
+	  if ((slave = open(path, O_RDONLY)) >= 0) {
+	    struct termios tio;
+	    
+	    tcgetattr(slave, &tio);
+	    cfmakeraw(&tio);
+	    tio.c_lflag &= ~(ICANON|ECHO|ECHOE|ECHOK|ECHONL|ECHOCTL|ECHOKE);
+	    tcsetattr(slave, TCSANOW, &tio);
+	    close(slave);
+	    slave = -1;
+	    
+	    tweaked = 1;
+	  }
+	}
+	if (pf[1].revents & POLLIN) {
+	  rc = read(pf[1].fd,
+		    &from_pty.data[from_pty.inuse],
+		    sizeof(from_pty.data) - from_pty.inuse);
+	  if (rc > 0)
+	    from_pty.inuse += rc;
+	}
+	if (pf[1].revents & POLLOUT) {
+	  rc = write(pf[1].fd, to_pty.data, to_pty.inuse);
+	  pack_buffer(&to_pty, rc);
+	}
+	if (pf[0].revents & POLLIN) {
+	  rc = readFunc(&to_pty.data[to_pty.inuse],
+			sizeof(to_pty.data) - to_pty.inuse);
+	  if (rc >= 0)
+	    to_pty.inuse += rc;
+	  else
+	    exit(0);
+	}
+	if (pf[0].revents & POLLOUT) {
+	  rc = writeFunc(from_pty.data, from_pty.inuse);
+	  if (rc < 0)
+	    exit(0);
+	  pack_buffer(&from_pty, rc);
+	}
+      }
+      
+      pf[0].events = 0;
+      if (to_pty.inuse < sizeof(to_pty.data))
+	pf[0].events |= POLLIN;
+      if (from_pty.inuse > 0)
+	pf[0].events |= POLLOUT;
+      
+      pf[1].events = POLLHUP;
+      if (from_pty.inuse < sizeof(from_pty.data))
+	pf[1].events |= POLLIN;
+      if (to_pty.inuse > 0)
+	pf[1].events |= POLLOUT;
+    }
+  }
+}
+
+#endif
diff --git a/tmcd/common/bootsubnodes b/tmcd/common/bootsubnodes
index 7739fe5425..f2b8dd4c0f 100755
--- a/tmcd/common/bootsubnodes
+++ b/tmcd/common/bootsubnodes
@@ -125,6 +125,38 @@ foreach my $subnode (keys(%subnodelist)) {
 	/^mote$/i && do {
 	    libsetup_setvnodeid($subnode);
 	    configtmcc("subnode", $subnode);
+	    tmcc(TMCCCMD_STATE, "BOOTING");
+	    tmcc(TMCCCMD_SUBCONFIG, undef, \@tmccresults) == 0
+		or die("*** $0:\n".
+		       "    Could not get subnode config from server!\n");
+	    my $BAUD = 0;
+	    my $CAPSERVER = "";
+	    my $CAPPORT = 0;
+	    foreach my $str (@tmccresults) {
+		chomp($str);
+	      SWITCH1: for ($str) {
+		  /^TYPE=([-\w.]+)$/ && do {
+		      if ($1 eq "mica2") {
+			  $BAUD = 57600;
+		      }
+		      last SWITCH1;
+		  };
+		  /^CAPSERVER=([-\w.]+)$/ && do {
+		      $CAPSERVER = $1;
+		      last SWITCH1;
+		  };
+		  /^CAPPORT=(\d+)$/ && do {
+		      $CAPPORT = $1;
+		      last SWITCH1;
+		  };
+		  printf STDERR "Unknown directive: $str\n";
+	      }
+	    }
+	    if (-x "$BINDIR/capture") {
+		system("$BINDIR/capture -a -s $BAUD -u '/usr/local/bin/uisp ".
+		       "-dprog=sggpio -dpart=ATmega128 --wr_fuse_e=ff ".
+		       "--erase --upload if=%s' $CAPSERVER:$CAPPORT tts/2");
+	    }
 	    tmcc(TMCCCMD_STATE, "ISUP");
 	    last SWITCH;
 	};
diff --git a/tmcd/common/config/GNUmakefile.in b/tmcd/common/config/GNUmakefile.in
index 5b3892f10f..17e1926d33 100644
--- a/tmcd/common/config/GNUmakefile.in
+++ b/tmcd/common/config/GNUmakefile.in
@@ -1,6 +1,6 @@
 #
 # EMULAB-COPYRIGHT
-# Copyright (c) 2000-2004 University of Utah and the Flux Group.
+# Copyright (c) 2000-2005 University of Utah and the Flux Group.
 # All rights reserved.
 #
 
@@ -22,7 +22,8 @@ SCRIPTS		= $(addprefix $(SRCDIR)/, \
 		    rc.tunnels rc.ifconfig rc.delays rc.hostnames \
 		    rc.syncserver rc.linkagent rc.mkelab rc.localize \
 		    rc.keys rc.trafgen rc.tarfiles rc.rpms rc.progagent \
-		    rc.startcmd rc.simulator rc.topomap rc.firewall)
+		    rc.startcmd rc.simulator rc.topomap rc.firewall \
+		    rc.tiptunnels)
 
 include $(OBJDIR)/Makeconf
 
diff --git a/tmcd/common/config/rc.config b/tmcd/common/config/rc.config
index e56078c56e..051b4fa9e6 100755
--- a/tmcd/common/config/rc.config
+++ b/tmcd/common/config/rc.config
@@ -1,7 +1,7 @@
 #!/usr/bin/perl -w
 #
 # EMULAB-COPYRIGHT
-# Copyright (c) 2004 University of Utah and the Flux Group.
+# Copyright (c) 2004, 2005 University of Utah and the Flux Group.
 # All rights reserved.
 #
 use English;
@@ -99,7 +99,7 @@ else {
 		    "rc.route", "rc.tunnels", "rc.ifconfig", "rc.delays",
 		    "rc.hostnames", "rc.syncserver", "rc.trafgen",
 		    "rc.tarfiles", "rc.rpms", "rc.progagent", "rc.linkagent",
-		    "rc.startcmd", "rc.simulator");
+		    "rc.tiptunnels", "rc.startcmd", "rc.simulator");
 }
 
 # Execute the action.
diff --git a/tmcd/common/config/rc.tiptunnels b/tmcd/common/config/rc.tiptunnels
new file mode 100644
index 0000000000..5c4506f16c
--- /dev/null
+++ b/tmcd/common/config/rc.tiptunnels
@@ -0,0 +1,139 @@
+#!/usr/bin/perl -w
+#
+# EMULAB-COPYRIGHT
+# Copyright (c) 2005 University of Utah and the Flux Group.
+# All rights reserved.
+#
+use English;
+use Getopt::Std;
+use POSIX qw(setsid);
+
+sub usage()
+{
+    print "Usage: " .
+	scriptname() . "boot|shutdown|reconfig|reset\n";
+    exit(1);
+}
+my $action  = "boot";
+
+# Turn off line buffering on output
+$| = 1;
+
+# Drag in path stuff so we can find emulab stuff.
+BEGIN { require "/etc/emulab/paths.pm"; import emulabpaths; }
+
+# Only root.
+if ($EUID != 0) {
+    die("*** $0:\n".
+	"    Must be root to run this script!\n");
+}
+
+#
+# Load the OS independent support library. It will load the OS dependent
+# library and initialize itself. 
+# 
+use libsetup;
+use libtmcc;
+use librc;
+
+#
+# Not all clients support this.
+#
+exit(0)
+    if (MFS() || PLAB() || JAILED());
+
+# Protos.
+sub doboot();
+sub doshutdown();
+sub doreconfig();
+sub docleanup();
+
+# Allow default above.
+if (@ARGV) {
+    $action = $ARGV[0];
+}
+
+# Execute the action.
+SWITCH: for ($action) {
+    /^boot$/i && do {
+	doboot();
+	last SWITCH;
+    };
+    /^shutdown$/i && do {
+	doshutdown();
+	last SWITCH;
+    };
+    /^reconfig$/i && do {
+	doreconfig();
+	last SWITCH;
+    };
+    /^reset$/i && do {
+	docleanup();
+	last SWITCH;
+    };
+    fatal("Invalid action: $action\n");
+}
+exit(0);
+
+#
+# Boot Action.
+#
+sub doboot()
+{
+    my @tiptunnels;
+
+    print STDOUT "Checking Testbed tiptunnel configuration ... \n";
+
+    if (gettiptunnelconfig(\@tiptunnels)) {
+	fatal("Could not get tiptunnel configuration from libsetup!");
+    }
+
+    foreach my $tiptunnel (@tiptunnels) {
+	if (-e "/var/run/tippty.$tiptunnel.pid") {
+	    print STDOUT "Tunnel for '$tiptunnel' is already running.\n";
+	}
+	else {
+	    system("$BINDIR/tippty $tiptunnel");
+	}
+    }
+
+    return;
+}
+
+#
+# Shutdown Action.
+#
+sub doshutdown()
+{
+    my @tiptunnels;
+
+    if (gettiptunnelconfig(\@tiptunnels)) {
+	fatal("Could not get tiptunnel configuration from libsetup!");
+    }
+
+    foreach my $tiptunnel (@tiptunnels) {
+	if (-e "/var/run/tippty.$tiptunnel.pid") {
+	    system("kill `cat /var/run/tippty.$tiptunnel.pid`");
+	    unlink "/var/run/tippty.$tiptunnel.pid";
+	}
+    }
+
+    return;
+}
+
+#
+# Node Reconfig Action (without rebooting).
+#
+sub doreconfig()
+{
+    # Shutdown tunnels before doing reconfig.
+    doshutdown();
+    return doboot();
+}
+
+#
+# Node cleanup action (node is reset to completely clean state).
+#
+sub docleanup()
+{
+}
diff --git a/tmcd/common/libsetup.pm b/tmcd/common/libsetup.pm
index 1a29f9cd17..d4ffc10b30 100644
--- a/tmcd/common/libsetup.pm
+++ b/tmcd/common/libsetup.pm
@@ -19,7 +19,7 @@ use Exporter;
 	 check_nickname	bootsetup startcmdstatus whatsmynickname 
 	 TBBackGround TBForkCmd vnodejailsetup plabsetup vnodeplabsetup
 	 jailsetup dojailconfig findiface libsetup_getvnodeid 
-	 ixpsetup libsetup_refresh gettopomap getfwconfig
+	 ixpsetup libsetup_refresh gettopomap getfwconfig gettiptunnelconfig
 
 	 TBDebugTimeStamp TBDebugTimeStampsOn
 
@@ -45,7 +45,7 @@ use libtmcc;
 #
 # BE SURE TO BUMP THIS AS INCOMPATIBILE CHANGES TO TMCD ARE MADE!
 #
-sub TMCD_VERSION()	{ 23; };
+sub TMCD_VERSION()	{ 24; };
 libtmcc::configtmcc("version", TMCD_VERSION());
 
 # Control tmcc timeout.
@@ -850,6 +850,49 @@ sub gettunnelconfig($)
     return 0;
 }
 
+#
+# Get tiptunnels configuration.
+#
+sub gettiptunnelconfig($)
+{
+    my ($rptr)   = @_;
+    my @tiptunnels = ();
+
+    if (tmcc(TMCCCMD_TIPTUNNELS, undef, \@tmccresults) < 0) {
+	warn("*** WARNING: Could not get tiptunnel config from server!\n");
+	return -1;
+    }
+
+    my $pat  = q(VNODE=([-\w.]+) SERVER=([-\w.]+) PORT=(\d+) );
+    $pat    .= q(KEYLEN=(\d+) KEY=([-\w.]+));
+
+    my $ACLDIR = "/var/log/tiplogs";
+
+    mkdir("$ACLDIR", 0755);
+    foreach my $str (@tmccresults) {
+	if ($str =~ /$pat/) {
+	    if (!open(ACL, "> $ACLDIR/$1.acl")) {
+		warn("*** WARNING: ".
+		     "gettiptunnelconfig: Could not open $ACLDIR/$1.acl\n");
+		return -1;
+	    }
+
+	    print ACL "host: $2\n";
+	    print ACL "port: $3\n";
+	    print ACL "keylen: $4\n";
+	    print ACL "key: $5\n";
+	    close(ACL);
+
+	    push(@tiptunnels, $1);
+	}
+	else {
+	    warn("*** WARNING: Bad tiptunnels line: $str\n");
+	}
+    }
+    @$rptr = @tiptunnels;
+    return 0;
+}
+
 my %fwvars = ();
 
 #
diff --git a/tmcd/common/libtmcc.pm b/tmcd/common/libtmcc.pm
index f3a45f7d3a..eb599e8359 100644
--- a/tmcd/common/libtmcc.pm
+++ b/tmcd/common/libtmcc.pm
@@ -30,6 +30,7 @@ use Exporter;
 	     TMCCCMD_FIREWALLINFO TMCCCMD_EMULABCONFIG
 	     TMCCCMD_CREATOR TMCCCMD_HOSTINFO TMCCCMD_LOCALIZATION
 	     TMCCCMD_BOOTERRNO TMCCCMD_BOOTLOG TMCCCMD_BATTERY TMCCCMD_USERENV
+	     TMCCCMD_TIPTUNNELS
 	     );
 
 # Must come after package declaration!
@@ -166,6 +167,7 @@ my %commandset =
       "bootlog"	        => {TAG => "bootlog"},
       "battery"	        => {TAG => "battery"},
       "userenv"	        => {TAG => "userenv"},
+      "tiptunnels"      => {TAG => "tiptunnels"},
     );
 
 #
@@ -218,6 +220,7 @@ sub TMCCCMD_BOOTERRNO   (){ $commandset{"booterrno"}->{TAG}; }
 sub TMCCCMD_BOOTLOG     (){ $commandset{"bootlog"}->{TAG}; }
 sub TMCCCMD_BATTERY     (){ $commandset{"battery"}->{TAG}; }
 sub TMCCCMD_USERENV     (){ $commandset{"userenv"}->{TAG}; }
+sub TMCCCMD_TIPTUNNELS  (){ $commandset{"tiptunnels"}->{TAG}; }
 
 #
 # Caller uses this routine to set configuration of this library
diff --git a/tmcd/common/rc.bootsetup b/tmcd/common/rc.bootsetup
index 8e94ec55cd..8f4f979010 100755
--- a/tmcd/common/rc.bootsetup
+++ b/tmcd/common/rc.bootsetup
@@ -361,6 +361,14 @@ sub doboot()
     if (tmcc(TMCCCMD_STATE, "ISUP") < 0) {
 	fatal("Error sending ISUP to Emulab Control!");
     }
+    if (-x "$BINDIR/bootsubnodes") {
+	print("Booting up subnodes\n");
+	# Foreground mode.
+	system("$BINDIR/bootsubnodes -f");
+	if ($?) {
+	    fatal("Error running $BINDIR/bootsubnodes");
+	}
+    }
 }
 
 #
diff --git a/tmcd/decls.h b/tmcd/decls.h
index 74160faa5f..046c16b4d7 100644
--- a/tmcd/decls.h
+++ b/tmcd/decls.h
@@ -26,4 +26,4 @@
  * NB: See ron/libsetup.pm. That is version 4! I'll merge that in. 
  */
 #define DEFAULT_VERSION		2
-#define CURRENT_VERSION		23
+#define CURRENT_VERSION		24
diff --git a/tmcd/linux/ixpboot b/tmcd/linux/ixpboot
index 8dcc299466..e4c1dca736 100755
--- a/tmcd/linux/ixpboot
+++ b/tmcd/linux/ixpboot
@@ -1,7 +1,7 @@
 #!/usr/bin/perl -wT
 #
 # EMULAB-COPYRIGHT
-# Copyright (c) 2000-2004 University of Utah and the Flux Group.
+# Copyright (c) 2000-2005 University of Utah and the Flux Group.
 # All rights reserved.
 #
 # TODO: Startup command in rc.ixp. Use old version.
@@ -94,11 +94,6 @@ if (!$debug && (my $childpid = TBBackGround($logname))) {
 print "Starting IXP bootup at " .
   POSIX::strftime("20%y/%m/%d %H:%M:%S", localtime()) . "\n";
 
-die("*** $0:\n".
-    "    Could not chdir to $ixpdir\n")
-    if (! -d $ixpdir ||
-	! chdir($ixpdir));
-
 # Tell the library what vnode we are messing with.
 libsetup_setvnodeid($ixpid);
 # Tell tmcc library too, although thats already been done with previous call.
@@ -125,6 +120,11 @@ if (! ixpsetup($ixpid)) {
     exit(0);
 }
 
+die("*** $0:\n".
+    "    Could not chdir to $ixpdir\n")
+    if (! -d $ixpdir ||
+	! chdir($ixpdir));
+
 #
 # Gen up a hostnames in the config dir.
 #
diff --git a/tmcd/tmcd.c b/tmcd/tmcd.c
index 09971c3d1d..27f9b0dbbc 100644
--- a/tmcd/tmcd.c
+++ b/tmcd/tmcd.c
@@ -236,6 +236,8 @@ COMMAND_PROTOTYPE(dobootlog);
 COMMAND_PROTOTYPE(dobattery);
 COMMAND_PROTOTYPE(dotopomap);
 COMMAND_PROTOTYPE(douserenv);
+COMMAND_PROTOTYPE(dotiptunnels);
+COMMAND_PROTOTYPE(dorelayconfig);
 
 /*
  * The fullconfig slot determines what routines get called when pushing
@@ -289,14 +291,14 @@ struct command {
 	{ "state",	  FULLCONFIG_NONE, 0, dostate},
 	{ "tunnels",	  FULLCONFIG_ALL,  F_ALLOCATED, dotunnels},
 	{ "vnodelist",	  FULLCONFIG_PHYS, 0, dovnodelist},
-	{ "subnodelist",  FULLCONFIG_PHYS, F_ALLOCATED, dosubnodelist},
+	{ "subnodelist",  FULLCONFIG_PHYS, 0, dosubnodelist},
 	{ "isalive",	  FULLCONFIG_NONE, F_REMUDP|F_MINLOG, doisalive},
 	{ "ipodinfo",	  FULLCONFIG_NONE, 0, doipodinfo},
 	{ "ntpinfo",	  FULLCONFIG_PHYS, 0, dontpinfo},
 	{ "ntpdrift",	  FULLCONFIG_NONE, 0, dontpdrift},
 	{ "jailconfig",	  FULLCONFIG_VIRT, F_ALLOCATED, dojailconfig},
 	{ "plabconfig",	  FULLCONFIG_VIRT, F_ALLOCATED, doplabconfig},
-	{ "subconfig",	  FULLCONFIG_NONE, F_ALLOCATED, dosubconfig},
+	{ "subconfig",	  FULLCONFIG_NONE, 0, dosubconfig},
         { "sdparams",     FULLCONFIG_PHYS, 0, doslothdparams},
         { "programs",     FULLCONFIG_ALL,  F_ALLOCATED, doprogagents},
         { "syncserver",   FULLCONFIG_ALL,  F_ALLOCATED, dosyncserver},
@@ -318,6 +320,7 @@ struct command {
 	{ "battery",      FULLCONFIG_NONE, F_REMUDP|F_MINLOG, dobattery},
 	{ "topomap",      FULLCONFIG_NONE, F_MINLOG|F_ALLOCATED, dotopomap},
 	{ "userenv",      FULLCONFIG_NONE, F_ALLOCATED, douserenv},
+	{ "tiptunnels",	  FULLCONFIG_ALL,  F_ALLOCATED, dotiptunnels},
 };
 static int numcommands = sizeof(command_array)/sizeof(struct command);
 
@@ -3657,13 +3660,15 @@ COMMAND_PROTOTYPE(dovnodelist)
  */
 COMMAND_PROTOTYPE(dosubnodelist)
 {
-	MYSQL_RES	*res;	
+	MYSQL_RES	*res;
 	MYSQL_ROW	row;
 	char		buf[MYBUFSIZE];
 	int		nrows;
 
-	res = mydb_query("select r.node_id,nt.class from reserved as r "
-			 "left join nodes as n on r.node_id=n.node_id "
+	if (vers <= 23)
+		return 0;
+
+	res = mydb_query("select n.node_id,nt.class from nodes as n "
                          "left join node_types as nt on nt.type=n.type "
                          "where nt.issubnode=1 and n.phys_nodeid='%s'",
                          2, reqp->nodeid);
@@ -4576,6 +4581,9 @@ COMMAND_PROTOTYPE(doplabconfig)
  */
 COMMAND_PROTOTYPE(dosubconfig)
 {
+	if (vers <= 23)
+		return 0;
+
 	if (!reqp->issubnode) {
 		error("SUBCONFIG: %s: Not a subnode\n", reqp->nodeid);
 		return 1;
@@ -4584,6 +4592,9 @@ COMMAND_PROTOTYPE(dosubconfig)
 	if (! strcmp(reqp->type, "ixp-bv")) 
 		return(doixpconfig(sock, reqp, rdata, tcp, vers));
 	
+	if (! strcmp(reqp->type, "mica2")) 
+		return(dorelayconfig(sock, reqp, rdata, tcp, vers));
+	
 	error("SUBCONFIG: %s: Invalid subnode class %s\n",
 	      reqp->nodeid, reqp->class);
 	return 1;
@@ -5863,7 +5874,7 @@ COMMAND_PROTOTYPE(douserenv)
 			 2, reqp->pid, reqp->eid);
 
 	if (!res) {
-		error("PROGRAM: %s: DB Error getting virt_user_environment\n",
+		error("USERENV: %s: DB Error getting virt_user_environment\n",
 		      reqp->nodeid);
 		return 1;
 	}
@@ -5882,7 +5893,94 @@ COMMAND_PROTOTYPE(douserenv)
 		
 		nrows--;
 		if (verbose)
-			info("PROGAGENTS: %s", buf);
+			info("USERENV: %s", buf);
+	}
+	mysql_free_result(res);
+	return 0;
+}
+
+/*
+ * Return tip tunnels for the node.
+ */
+COMMAND_PROTOTYPE(dotiptunnels)
+{
+	MYSQL_RES	*res;	
+	MYSQL_ROW	row;
+	char		buf[MYBUFSIZE];
+	int		nrows;
+
+	res = mydb_query("select vtt.vnode,tl.server,tl.portnum,tl.keylen,"
+			 "tl.keydata "
+			 "from virt_tiptunnels as vtt "
+			 "left join reserved as r on r.vname=vtt.vnode and "
+			 "  r.pid=vtt.pid and r.eid=vtt.eid "
+			 "left join tiplines as tl on tl.node_id=r.node_id "
+			 "where vtt.pid='%s' and vtt.eid='%s' and "
+			 "vtt.host='%s'",
+			 5, reqp->pid, reqp->eid, reqp->nickname);
+
+	if (!res) {
+		error("TIPTUNNELS: %s: DB Error getting virt_tiptunnels\n",
+		      reqp->nodeid);
+		return 1;
+	}
+	if ((nrows = (int)mysql_num_rows(res)) == 0) {
+		mysql_free_result(res);
+		return 0;
+	}
+	
+	while (nrows) {
+		row = mysql_fetch_row(res);
+
+		if (row[1]) {
+			OUTPUT(buf, sizeof(buf),
+			       "VNODE=%s SERVER=%s PORT=%s KEYLEN=%s KEY=%s\n",
+			       row[0], row[1], row[2], row[3], row[4]);
+			client_writeback(sock, buf, strlen(buf), tcp);
+		}
+		
+		nrows--;
+		if (verbose)
+			info("TIPTUNNELS: %s", buf);
+	}
+	mysql_free_result(res);
+	return 0;
+}
+
+COMMAND_PROTOTYPE(dorelayconfig)
+{
+	MYSQL_RES	*res;	
+	MYSQL_ROW	row;
+	char		buf[MYBUFSIZE];
+	int		nrows;
+
+	res = mydb_query("select tl.server,tl.portnum from tiplines as tl "
+			 "where tl.node_id='%s'",
+			 2, reqp->nodeid);
+
+	if (!res) {
+		error("RELAYCONFIG: %s: DB Error getting relay config\n",
+		      reqp->nodeid);
+		return 1;
+	}
+	if ((nrows = (int)mysql_num_rows(res)) == 0) {
+		mysql_free_result(res);
+		return 0;
+	}
+	
+	while (nrows) {
+		row = mysql_fetch_row(res);
+
+		OUTPUT(buf, sizeof(buf),
+		       "TYPE=%s\n"
+		       "CAPSERVER=%s\n"
+		       "CAPPORT=%s\n",
+		       reqp->type, row[0], row[1]);
+		client_writeback(sock, buf, strlen(buf), tcp);
+		
+		nrows--;
+		if (verbose)
+			info("RELAYCONFIG: %s", buf);
 	}
 	mysql_free_result(res);
 	return 0;
diff --git a/utils/GNUmakefile.in b/utils/GNUmakefile.in
index be52a9f64e..8331fd6906 100644
--- a/utils/GNUmakefile.in
+++ b/utils/GNUmakefile.in
@@ -15,7 +15,7 @@ include $(OBJDIR)/Makeconf
 SUBDIRS		= nsgen
 
 BIN_SCRIPTS	= delay_config sshtb create_image node_admin link_config \
-                  setdest webcopy
+                  setdest loghole webcopy
 SBIN_SCRIPTS	= vlandiff vlansync withadminprivs export_tables cvsupd.pl \
                   eventping grantnodetype import_commitlog dhcpd_wrapper \
 		  opsreboot deletenode node_statewait grabwebcams \
diff --git a/utils/loghole.in b/utils/loghole.in
index 77b50c25b0..a359e58a50 100644
--- a/utils/loghole.in
+++ b/utils/loghole.in
@@ -45,7 +45,7 @@ EXPDIR_FMT = os.path.join("/", DIRS["proj"], "%(PID)s", "exp", "%(EID)s")
 
 GLOBAL_LOGS = [
     "event-sched.log", "feedback.log", "../tbdata/feedback_data.tcl",
-    "digest.out"
+    "digest.out", "report.mail"
     ]
 
 PID = None
@@ -729,6 +729,10 @@ def do_show(args):
                 print "  Keep-until:\t\t%s" % cp.get("MAIN", "keep-until")
                 print ("  Keep-atleast:\t\t%s days" %
                        cp.get("MAIN", "keep-atleast"))
+                if "report.mail" in lh.namelist():
+                    print "Report:"
+                    print lh.read("report.mail")
+                    pass
                 for name in lh.namelist():
                     if re.match(r'loghole-comment.*\.txt', name):
                         before = 38 - (len(name) / 2)
diff --git a/xmlrpc/emulabserver.py.in b/xmlrpc/emulabserver.py.in
index 2b14fa57e6..67ef5c0065 100755
--- a/xmlrpc/emulabserver.py.in
+++ b/xmlrpc/emulabserver.py.in
@@ -108,7 +108,10 @@ virtual_tables = {
                                     "attrs" : [ "vname" ]},
     "eventlist"                 : { "rows"  : None, 
                                     "tag"   : "events",
-                                    "attrs" : [ "vname" ]}
+                                    "attrs" : [ "vname" ]},
+    "virt_tiptunnels"           : { "rows"  : None,
+                                    "tag"   : "tiptunnels",
+                                    "attrs" : [ "host", "vnode" ]},
     }
     
 # Base class for emulab specific exceptions.
@@ -414,6 +417,7 @@ class emulab:
                 ob["y2"] = ob["y2"] / ppm
                 ob["z2"] = ob["z2"] / ppm
                 pass
+            scrubdict(ob)
             pass
         
         return EmulabResponse(RESPONSE_SUCCESS,
@@ -1624,7 +1628,8 @@ class experiment:
                     tmp["load_15min"] = res[9]
                     pass
                 tmp["erole"] = res[11]
-                mapping[res[0]] = tmp
+                mapping[res[0]] = scrubdict(tmp,
+                                            defaultvals={ "status" : "up" })
                 pass
             pass
 
@@ -2573,7 +2578,10 @@ class experiment:
                 if not ipres or len(ipres) == 0:
                     continue
 
-                ipaddr = ipres[0][0];
+                ipaddr = ipres[0][0]
+                if not ipaddr:
+                    ipaddr = ""
+                    pass
                 nodeid = agent[2]
                 pass 
             
@@ -3845,7 +3853,7 @@ def nfspath(value):
 #
 def scrubdict(retval, prunelist=[], defaultvals={}):
     for key in retval.keys():
-        if not retval[key] or key in prunelist:
+        if (retval[key] == None) or key in prunelist:
             if key in defaultvals:
                 retval[key] = defaultvals[key]
                 pass
-- 
GitLab