diff --git a/gtests/net/packetdrill/logging.c b/gtests/net/packetdrill/logging.c
index 546b5d1188a87f1145378e5c5bc30b6ca45a2aba..730add4b8c8683cf8631a0b18cefa231ef397013 100644
--- a/gtests/net/packetdrill/logging.c
+++ b/gtests/net/packetdrill/logging.c
@@ -22,12 +22,13 @@
  * Logging and output functions.
  */
 
-#include "logging.h"
+#include "run.h"
+#include "system.h"
 
 #include <stdarg.h>
 #include <stdlib.h>
 
-extern void die(char *format, ...)
+extern void __attribute__((noreturn)) die(char *format, ...)
 {
 	va_list ap;
 
@@ -35,12 +36,16 @@ extern void die(char *format, ...)
 	vfprintf(stderr, format, ap);
 	va_end(ap);
 
+	run_cleanup_command();
+
 	exit(EXIT_FAILURE);
 }
 
-void die_perror(char *message)
+void __attribute__((noreturn)) die_perror(char *message)
 {
 	perror(message);
 
+	run_cleanup_command();
+
 	exit(EXIT_FAILURE);
 }
diff --git a/gtests/net/packetdrill/logging.h b/gtests/net/packetdrill/logging.h
index a537ffa4e6dc42b9e830693a422e708b1017a253..2e17bd097c522bb58cbebd1b7ffe686fcfc3368c 100644
--- a/gtests/net/packetdrill/logging.h
+++ b/gtests/net/packetdrill/logging.h
@@ -42,9 +42,9 @@ extern int debug_logging;
 #endif /* DEBUG */
 
 /* Log the message to stderr and then exit with a failure status code. */
-extern void die(char *format, ...);
+extern void __attribute__((noreturn)) die(char *format, ...);
 
 /* Call perror() with message and then exit with a failure status code. */
-extern void die_perror(char *message);
+extern void __attribute__((noreturn)) die_perror(char *message);
 
 #endif /* __LOGGING_H__ */
diff --git a/gtests/net/packetdrill/parser.y b/gtests/net/packetdrill/parser.y
index 54c4ee119d09cf46af5179fbe09c21ee2aa996e1..2bff622a50bed1691ea5e52ef7c596c797a6ec87 100644
--- a/gtests/net/packetdrill/parser.y
+++ b/gtests/net/packetdrill/parser.y
@@ -123,6 +123,7 @@ extern char *yytext;
 extern int yylex(void);
 extern int yyparse(void);
 extern int yywrap(void);
+extern const char *cleanup_cmd;
 
 /* This mutex guards all parser global variables declared in this file. */
 pthread_mutex_t parser_mutex = PTHREAD_MUTEX_INITIALIZER;
@@ -811,7 +812,7 @@ static struct tcp_option *new_tcp_exp_fast_open_option(const char *cookie_string
 %%  /* The grammar follows. */
 
 script
-: opt_options opt_init_command events {
+: opt_options opt_init_command events opt_cleanup_command {
 	$$ = NULL;		/* The parser output is in out_script */
 }
 ;
@@ -5851,3 +5852,15 @@ null
 	$$ = new_expression(EXPR_NULL);
 }
 ;
+
+opt_cleanup_command
+:			{ }
+| cleanup_command	{ }
+;
+
+cleanup_command
+: command_spec {
+	out_script->cleanup_command = $1;
+	cleanup_cmd = out_script->cleanup_command->command_line;
+}
+;
diff --git a/gtests/net/packetdrill/run.c b/gtests/net/packetdrill/run.c
index f079e185a624c57d07dc0e80d8bf9356449e105c..151301ed756777760c303040706c76217b30986f 100644
--- a/gtests/net/packetdrill/run.c
+++ b/gtests/net/packetdrill/run.c
@@ -68,6 +68,15 @@ const int MAX_SPIN_USECS = 100;
 const int MAX_SPIN_USECS = 20;
 #endif
 
+/* Global bool init_cmd_exed */
+bool init_cmd_exed = false;
+
+/* Final command to always execute at end of script, in order to clean up: */
+const char *cleanup_cmd;
+
+/* Path of currently-executing script, for use in cleanup command errors: */
+const char *script_path;
+
 static struct state *state = NULL;
 
 struct state *state_new(struct config *config,
@@ -514,7 +523,8 @@ static s64 schedule_start_time_usecs(void)
 #endif
 }
 
-void signal_handler(int signal_number) {
+void signal_handler(int signal_number)
+{
 	if (state != NULL) {
 		close_all_sockets(state);
 		if (state->netdev != NULL) {
@@ -524,6 +534,28 @@ void signal_handler(int signal_number) {
 	die("Handled signal %d\n", signal_number);
 }
 
+/* Run final command we always execute at end of script, to clean up.  If there
+ * is a cleanup command at the end of a packetdrill script, we execute that no
+ * matter whether the test passes or fails. This makes the cleanup command a
+ * good place to undo any sysctl settings the script changed, for example.
+ */
+int run_cleanup_command(void)
+{
+	if (cleanup_cmd != NULL && init_cmd_exed) {
+		char *error = NULL;
+
+		if (safe_system(cleanup_cmd, &error)) {
+			fprintf(stderr,
+				"%s: error executing cleanup command: %s\n",
+				 script_path, error);
+			free(error);
+			return STATUS_ERR;
+		}
+	}
+	return STATUS_OK;
+}
+
+
 void run_script(struct config *config, struct script *script)
 {
 	char *error = NULL;
@@ -548,6 +580,8 @@ void run_script(struct config *config, struct script *script)
 	/* This interpreter loop runs for local mode or wire client mode. */
 	assert(!config->is_wire_server);
 
+	script_path = config->script_path;
+
 	/* How we use the network is of course a little different in
 	 * each of the two cases....
 	 */
@@ -563,12 +597,16 @@ void run_script(struct config *config, struct script *script)
 		wire_client_init(state->wire_client, config, script, state);
 	}
 
+	init_cmd_exed = false;
 	if (script->init_command != NULL) {
 		if (safe_system(script->init_command->command_line,
 				&error)) {
-			die("%s: error executing init command: %s\n",
-			    config->script_path, error);
+			asprintf(&error, "%s: error executing init command: %s\n",
+				 config->script_path, error);
+			free(error);
+			exit(EXIT_FAILURE);
 		}
+		init_cmd_exed = true;
 	}
 
 	signal(SIGPIPE, SIG_IGN);	/* ignore EPIPE */
@@ -630,6 +668,9 @@ void run_script(struct config *config, struct script *script)
 	if (state->wire_client != NULL)
 		wire_client_next_event(state->wire_client, NULL);
 
+	if (run_cleanup_command() == STATUS_ERR)
+		exit(EXIT_FAILURE);
+
 	if (code_execute(state->code, &error)) {
 		char *script_path = strdup(state->config->script_path);
 		state_free(state, 1);
diff --git a/gtests/net/packetdrill/run.h b/gtests/net/packetdrill/run.h
index 981692102cf1cb7bc326bcf7b0793316bba3b123..6acb518b397a222ac605fd83b751b940194156f3 100644
--- a/gtests/net/packetdrill/run.h
+++ b/gtests/net/packetdrill/run.h
@@ -184,4 +184,7 @@ extern void set_scheduling_priority(void);
 /* Try to pin our pages into RAM. */
 extern void lock_memory(void);
 
+/* Run final command we always execute at end of script, to clean up. */
+extern int run_cleanup_command(void);
+
 #endif /* __RUN_H__ */
diff --git a/gtests/net/packetdrill/script.h b/gtests/net/packetdrill/script.h
index 31c69416d9aa01cf2120d0c60529190ee96dd6d8..228442b08189386e24e20d9675de3d50d49773f1 100644
--- a/gtests/net/packetdrill/script.h
+++ b/gtests/net/packetdrill/script.h
@@ -763,10 +763,14 @@ struct script {
 	struct option_list *option_list;    /* linked list of options */
 	struct command_spec *init_command;  /* untimed initialization command */
 	struct event	*event_list;	    /* linked list of all events */
+	struct command_spec *cleanup_command;  /* untimed cleanup command */
 	char		*buffer;	    /* raw input text of the script */
 	int		length;		    /* number of bytes in the script */
 };
 
+/* Global pointer for final command we always execute at end of script: */
+extern const char *cleanup_cmd;
+
 /* A table entry mapping a bit mask to its human-readable name.
  * A table of such mappings must be terminated with a struct with a
  * NULL name.