diff --git a/exec/addfiles.js b/exec/addfiles.js
index 37da051ee8ce66218367251822c75ebab23651b3..0084acf5e491d4533568c4d3172908d1682e2c79 100644
--- a/exec/addfiles.js
+++ b/exec/addfiles.js
@@ -31,6 +31,16 @@ function archive_date(file)
 	return t;
 }
 
+function proper_lib_name(name)
+{
+	for(var i in file_area.lib_list) {
+		var lib = file_area.lib_list[i];
+		if(lib.name.toLowerCase() == name.toLowerCase())
+			return lib.name;
+	}
+	return name;
+}
+
 var uploader;
 var listfile;
 var date_fmt;
@@ -47,29 +57,36 @@ for(var i = 0; i < argc; i++) {
 		while(opt[0] == '-')
 			opt = opt.slice(1);
 		if(opt == '?' || opt.toLowerCase() == "help") {
-			writeln("usage: [-options] [dir-code] [listfile] [desc-off]");
+			writeln("usage: [dir-spec] [-options] [listfile] [desc-off]");
+			writeln();
+			writeln("dir-spec:");
+			writeln("  -all              Add files in all directories of all libraries (implies -auto)");
+			writeln("  -lib=<name>     * Add files in all directories of specified library (implies -auto)");
+			writeln("  -dir=<code>,... * Add files in multiple specified directories");
+			writeln("   dir-code         Internal code of (one and only) directory to add files to");
+			writeln("                  * indicates parameters that can be combined and/or specified multiple times");
+			writeln("   Note: If no directories are specified, one will be prompted for.");
+			writeln();
 			writeln("options:");
-			writeln("  -all            add files in all libraries/directories (implies -auto)");
-			writeln("  -lib=<name>     add files in all directories of specified library (implies -auto)");
-			writeln("  -auto           add files only to directories that have Auto-ADDFILES enabled");
-			writeln("  -from=<name>    specify uploader's user name (may require quotes)");
-			writeln("  -file=<name>    specify files to add (wildcards supported, default: *)");
-			writeln("  -ex=<filename>  add to excluded filename list");
-			writeln("                  (default: " + default_excludes.join(',') + ")");
-			writeln("  -diz            always extract/use description in archive");
-			writeln("  -update         update existing file entries (default is to skip them)");
-			writeln("  -readd          re-add existing file entries (so they appear as newly-uploaded");
-			writeln("  -date[=fmt]     include today's date in description");
-			writeln("  -fdate[=fmt]    include file's date in description");
-			writeln("  -adate[=fmt]    include newest archived file date in description");
-			writeln("                  (fmt = optional strftime date/time format string)");
-			writeln("  -delete         delete list after import");
-			writeln("  -v              increase verbosity of output");
-			writeln("  -debug          enable debug output");
-			writeln("optional:");
-			writeln("  dir-code:       File directory internal code");
-			writeln("  listfile:       Name of listfile (e.g. FILES.BBS)");
-			writeln("  desc-off:       Description character offset (number)");
+			writeln("  -auto             Add files only to directories that have Auto-ADDFILES enabled (in SCFG)");
+			writeln("  -from=<name>      Specify uploader's user name (may require quotes)");
+			writeln("  -file=<name>      Specify files to add (wildcards supported, default: *)");
+			writeln("  -ex=<filename>    Add to excluded filename list");
+			writeln("                    (default: " + default_excludes.join(',') + ")");
+			writeln("  -diz              Always extract/use description in archive (e.g. FILE_ID.DIZ)");
+			writeln("  -update           Update existing file entries (default is to skip them)");
+			writeln("  -readd            Re-add existing file entries (so they appear as newly-uploaded");
+			writeln("  -date[=fmt]       Include today's date in description");
+			writeln("  -fdate[=fmt]      Include file's date in description");
+			writeln("  -adate[=fmt]      Include newest archived file date in description");
+			writeln("                    (fmt = optional strftime date/time format string)");
+			writeln("  -delete           Delete list after import");
+			writeln("  -v                Increase verbosity of output");
+			writeln("  -debug            Enable debug output");
+			writeln();
+			writeln("optional listfile parameters:");
+			writeln("   listfile         Name of listfile to import (e.g. FILES.BBS)");
+			writeln("   desc-off         Description character-offset in listfile (e.g. 40)");
 			exit(0);
 		}
 		if(opt.indexOf("ex=") == 0) {
@@ -77,9 +94,12 @@ for(var i = 0; i < argc; i++) {
 			continue;
 		}
 		if(opt.indexOf("lib=") == 0) {
-			var libname = opt.slice(4);
+			var libname = proper_lib_name(opt.slice(4));
 			if(!file_area.lib[libname]) {
 				alert("Library not found: " + libname);
+				writeln("Valid library names:");
+				for(var i in file_area.lib)
+					writeln("\t" + file_area.lib[i].name);
 				exit(1);
 			}
 			for(var j = 0; j < file_area.lib[libname].dir_list.length; j++)
@@ -87,6 +107,10 @@ for(var i = 0; i < argc; i++) {
 			options.auto = true;
 			continue;
 		}
+		if(opt.indexOf("dir=") == 0) {
+			dir_list.push.apply(dir_list, opt.slice(4).split(','));
+			continue;
+		}
 		if(opt.indexOf("file=") == 0) {
 			include = opt.slice(5);
 			continue;
@@ -111,6 +135,7 @@ for(var i = 0; i < argc; i++) {
 			continue;
 		}
 		if(opt == "all") {
+			dir_list.length = 0;
 			for(var dir in file_area.dir)
 				dir_list.push(dir);
 			options.auto = true;
@@ -153,8 +178,8 @@ var updated = 0;
 var renamed = 0;
 var missing = [];
 for(var d = 0; d < dir_list.length; d++) {
-	
-	var code = dir_list[d];
+
+	var code = dir_list[d].toLowerCase();
 	var dir = file_area.dir[code];
 	if(!dir) {
 		alert("Directory '" + code + "' does not exist in configuration");
@@ -163,7 +188,7 @@ for(var d = 0; d < dir_list.length; d++) {
 	if(options.auto && (dir.settings & DIR_NOAUTO))
 		continue;
 	writeln("Adding files to " + dir.lib_name + " " + dir.name);
-	
+
 	var filebase = new FileBase(code);
 	if(!filebase.open("r")) {
 		alert("Failed to open: " + filebase.file);
@@ -208,7 +233,6 @@ for(var d = 0; d < dir_list.length; d++) {
 	}
 	file_list = file_list.filter(function(obj) { return wildmatch(obj.name, include); });
 
-	
 	for(var i = 0; i < file_list.length; i++) {
 		var file = file_list[i];
 		file.from = uploader;
diff --git a/src/comio/comio_nix.c b/src/comio/comio_nix.c
index af7098fc31f67f0b4c276a3369ab79999051d627..cc3a66217fbdf469e2c65a6db2f4bfd006e054bc 100644
--- a/src/comio/comio_nix.c
+++ b/src/comio/comio_nix.c
@@ -362,15 +362,27 @@ bool comSetBits(COM_HANDLE handle, size_t byteSize, size_t stopBits)
 	switch(byteSize) {
 		case 5:
 			t.c_cflag |= CS5;
+#ifdef ISTRIP
+			t.c_iflag |= ISTRIP;
+#endif
 			break;
 		case 6:
 			t.c_cflag |= CS6;
+#ifdef ISTRIP
+			t.c_iflag |= ISTRIP;
+#endif
 			break;
 		case 7:
 			t.c_cflag |= CS7;
+#ifdef ISTRIP
+			t.c_iflag |= ISTRIP;
+#endif
 			break;
 		default:
 			t.c_cflag |= CS8;
+#ifdef ISTRIP
+			t.c_iflag &= ~(ISTRIP);
+#endif
 			break;
 	}
 	if(stopBits == 2)
diff --git a/src/sbbs3/logfile.cpp b/src/sbbs3/logfile.cpp
index 29d986902a8cd46584532bba8e8c2f3ba7b520ca..6692f853d636c9e84ac81c63d8303dd46a86fd0a 100644
--- a/src/sbbs3/logfile.cpp
+++ b/src/sbbs3/logfile.cpp
@@ -271,14 +271,19 @@ void sbbs_t::logch(char ch, bool comma)
 void sbbs_t::errormsg(int line, const char* function, const char *src, const char* action, const char *object
 					  ,int access, const char *extinfo)
 {
-    char	str[2048];
+	char	repeat[128] = "";
 	char	errno_str[128];
 	char	errno_info[256] = "";
+	static const char* lastfunc;
+	static int lastline;
+	static time_t lasttime;
+	static uint repeat_count;
 
 	/* prevent recursion */
 	if(errormsg_inside)
 		return;
 	errormsg_inside=true;
+	now=time(NULL);
 
 	if(errno != 0 && strcmp(action, ERR_CHK) != 0)
 		safe_snprintf(errno_info, sizeof(errno_info), "%d (%s) "
@@ -291,25 +296,36 @@ void sbbs_t::errormsg(int line, const char* function, const char *src, const cha
 	#endif
 		);
 
-	safe_snprintf(str,sizeof(str),"ERROR %s"
+	int level = LOG_ERR;
+	if(function == lastfunc && line == lastline) {
+		++repeat_count;
+		snprintf(repeat, sizeof repeat, "[x%u]", repeat_count + 1);
+		// De-duplicate by reducing severity of log messages
+		if((now - lasttime) < 12*60*60)
+			level = LOG_WARNING;
+	} else
+		repeat_count = 0;
+	lastfunc = function;
+	lastline = line;
+	lasttime = now;
+	lprintf(level, "!ERROR%s %s"
 		"in %s line %u (%s) %s \"%s\" access=%d %s%s"
+		,repeat
 		,errno_info
 		,src, line, function, action, object, access
 		,extinfo==NULL ? "":"info="
 		,extinfo==NULL ? "":extinfo);
 
-	lprintf(LOG_ERR, "!%s", str);
 	if(online == ON_REMOTE) {
 		int savatr=curatr;
 		attr(cfg.color[clr_err]);
-		bprintf("\7\r\n!ERROR %s %s\r\n", action, object);   /* tell user about error */
+		bprintf("\7\r\n!ERROR%s %s %s\r\n", repeat, action, object);   /* tell user about error */
 		bputs("\r\nThe sysop has been notified.\r\n");
 		pause();
 		attr(savatr);
 		CRLF;
 	}
-	safe_snprintf(str,sizeof(str),"ERROR %s %s", action, object);
-	if(cfg.node_num>0) {
+	if(repeat_count == 0 && cfg.node_num>0) {
 		if(getnodedat(cfg.node_num,&thisnode, true)) {
 			if(thisnode.errors<UCHAR_MAX)
 				thisnode.errors++;
@@ -317,12 +333,14 @@ void sbbs_t::errormsg(int line, const char* function, const char *src, const cha
 			putnodedat(cfg.node_num,&thisnode);
 		}
 	}
-	now=time(NULL);
 
 	if(logfile_fp!=NULL) {
-		if(logcol!=1)
-			fputs(log_line_ending, logfile_fp);
-		fprintf(logfile_fp,"!! %s%s", str, log_line_ending);
+		fprintf(logfile_fp,"%s!! ERROR%s %s %s%s"
+			,logcol == 1 ? "" : log_line_ending
+			,repeat
+			,action
+			,object
+			,log_line_ending);
 		logcol=1;
 		fflush(logfile_fp);
 	}
diff --git a/src/ssh/CMakeLists.txt b/src/ssh/CMakeLists.txt
index f4b2e184ab14b0fa6a30d868a0df3883a10c7900..8f1c2905f53910b70da5cfe44cf7bad8ecf6d6c0 100644
--- a/src/ssh/CMakeLists.txt
+++ b/src/ssh/CMakeLists.txt
@@ -32,4 +32,7 @@ target_link_libraries(deuce-ssh_static INTERFACE stdthreads)
 target_link_libraries(deuce-ssh_shared PRIVATE ${OPENSSL_CRYPTO_LIBRARIES})
 target_link_libraries(deuce-ssh_shared PRIVATE stdthreads)
 
+add_executable(client EXCLUDE_FROM_ALL client.c)
+target_link_libraries(client PRIVATE stdthreads deuce-ssh)
+
 install(TARGETS deuce-ssh FILE_SET HEADERS)
diff --git a/src/ssh/client.c b/src/ssh/client.c
new file mode 100644
index 0000000000000000000000000000000000000000..2203cba333900dfe03b3fb80d9dbd70e7444df60
--- /dev/null
+++ b/src/ssh/client.c
@@ -0,0 +1,179 @@
+#include <netinet/in.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <errno.h>
+#include <poll.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <threads.h>
+
+#include "ssh.h"
+
+int sock = -1;
+size_t txbufsz;
+size_t txbuflen;
+uint8_t *txbuf;
+atomic_bool *tx_terminate;
+cnd_t tx_cnd;
+mtx_t tx_mtx;
+struct deuce_ssh_session sess;
+
+int
+tx_thread(void *arg)
+{
+	while ((tx_terminate == NULL || *tx_terminate == false) && (sock != -1)) {
+		mtx_lock(&tx_mtx);
+		if (txbufsz == 0)
+			cnd_wait(&tx_cnd, &tx_mtx);
+		mtx_unlock(&tx_mtx);
+		struct pollfd fd = {
+			.fd = sock,
+			.events = POLLOUT | POLLRDHUP,
+			.revents = 0,
+		};
+		if (poll(&fd, 1, 5000) == 1) {
+			if (fd.revents & POLLOUT) {
+				mtx_lock(&tx_mtx);
+				ssize_t sent = send(sock, txbuf, txbuflen, MSG_NOSIGNAL);
+				if (sent > 0) {
+					memmove(txbuf, &txbuf[sent], txbuflen - sent);
+					txbuflen -= sent;
+				}
+				else if (sent == -1) {
+					switch(errno) {
+						case EAGAIN:
+						case ENOBUFS:
+							break;
+						default:
+							shutdown(sock, SHUT_WR);
+							*tx_terminate = true;
+							break;
+					}
+				}
+				mtx_unlock(&tx_mtx);
+			}
+			if (fd.revents & (POLLHUP | POLLRDHUP)) {
+				shutdown(sock, SHUT_WR);
+				*tx_terminate = true;
+			}
+		}
+	}
+	return 0;
+}
+
+static int
+tx(uint8_t *buf, size_t bufsz, atomic_bool *terminate, void *cbdata)
+{
+	mtx_lock(&tx_mtx);
+	if (txbuflen + bufsz > txbufsz) {
+		uint8_t *nb = realloc(txbuf, txbuflen + bufsz);
+		if (nb == NULL) {
+			mtx_unlock(&tx_mtx);
+			return DEUCE_SSH_ERROR_ALLOC;
+		}
+		else {
+			tx_terminate = terminate;
+			txbuf = nb;
+			memcpy(&txbuf[txbuflen], buf, bufsz);
+			txbufsz = txbuflen + bufsz;
+			if (txbuflen == 0)
+				cnd_signal(&tx_cnd);
+			txbuflen += bufsz;
+		}
+	}
+	mtx_unlock(&tx_mtx);
+	return 0;
+}
+
+static int
+rx(uint8_t *buf, size_t bufsz, atomic_bool *terminate, void *cbdata)
+{
+	size_t rxlen = 0;
+
+	while ((*terminate == false) && (sock != -1) && rxlen < bufsz) {
+		struct pollfd fd = {
+			.fd = sock,
+			.events = POLLIN | POLLRDHUP,
+			.revents = 0,
+		};
+		if (poll(&fd, 1, 5000) == 1) {
+			if (fd.revents & POLLIN) {
+				ssize_t received = recv(sock, &buf[rxlen], bufsz - rxlen, MSG_DONTWAIT);
+				if (received > 0) {
+					rxlen += received;
+				}
+				else if (received == -1) {
+					switch(errno) {
+						case EAGAIN:
+						case EINTR:
+							break;
+						default:
+							shutdown(sock, SHUT_RD);
+							*terminate = true;
+							break;
+					}
+				}
+			}
+			if (fd.revents & (POLLHUP | POLLRDHUP)) {
+				shutdown(sock, SHUT_RD);
+				*terminate = true;
+			}
+		}
+	}
+	return 0;
+}
+
+static int
+rxline(uint8_t *buf, size_t bufsz, size_t *bytes_received, atomic_bool *terminate, void *cbdata)
+{
+	size_t pos = 0;
+	bool lastcr = false;
+
+	for (;;) {
+		int res = rx(&buf[pos], 1, terminate, cbdata);
+		if (res < 0)
+			return res;
+		if (buf[pos] == '\r')
+			lastcr = true;
+		if (buf[pos] == '\n' && lastcr) {
+			*bytes_received = pos + 1;
+			return 0;
+		}
+		if (pos + 1 < bufsz)
+			pos++;
+	}
+}
+
+static int
+extra_line(uint8_t *buf, size_t bufsz, void *cbdata)
+{
+	fprintf(stdout, "%.*s\n", (int)bufsz, buf);
+	return 0;
+}
+
+
+int main(int argc, char **argv)
+{
+	struct sockaddr_in sa = {
+		.sin_len = sizeof(struct sockaddr_in),
+		.sin_family = AF_INET,
+		.sin_port = htons(22),
+		.sin_addr = htonl(0x7f000001),
+	};
+	mtx_init(&tx_mtx, mtx_plain);
+	cnd_init(&tx_cnd);
+	sock = socket(PF_INET, SOCK_STREAM, 0);
+	if (sock == -1)
+		return 1;
+	
+	if (connect(sock, (struct sockaddr *)&sa, sa.sin_len) == -1)
+		return 1;
+	if (deuce_ssh_transport_set_callbacks(tx, rx, rxline, extra_line) != 0)
+		return 1;
+	thrd_t thr;
+	thrd_create(&thr, tx_thread, NULL);
+	deuce_ssh_session_init(&sess);
+	int res;
+	thrd_join(thr, &res);
+}
diff --git a/src/ssh/ssh-trans.c b/src/ssh/ssh-trans.c
index 5fa1ab01a2e60f9f94b1b3b251adc463f5d340a0..91d2eda8daced2569876bc28146070f8479d1b45 100644
--- a/src/ssh/ssh-trans.c
+++ b/src/ssh/ssh-trans.c
@@ -5,7 +5,22 @@
 #include "ssh.h"
 #include "ssh-trans.h"
 
+typedef struct deuce_ssh_transport_global_config {
+	atomic_bool used;
+	const char *software_version;
+	const char *version_comment;
+	size_t kex_entries;
+	char **kex_name;
+	deuce_ssh_kex_handler_t *kex_handler;
+	deuce_ssh_kex_cleanup_t *kex_cleanup;
+	int (*tx)(uint8_t *buf, size_t bufsz, atomic_bool *terminate, void *cbdata);
+	int (*rx)(uint8_t *buf, size_t bufsz, atomic_bool *terminate, void *cbdata);
+	int (*rx_line)(uint8_t *buf, size_t bufsz, size_t *bytes_received, atomic_bool *terminate, void *cbdata);
+	int (*extra_line_cb)(uint8_t *buf, size_t bufsz, void *cbdata);
+} *deuce_ssh_transport_global_config_t;
+
 static const char * const sw_ver = "DeuceSSH-0.0";
+static struct deuce_ssh_transport_global_config gconf;
 
 static inline bool
 has_nulls(uint8_t *buf, size_t buflen)
@@ -17,7 +32,7 @@ static inline bool
 missing_crlf(uint8_t *buf, size_t buflen)
 {
 	assert(buflen >= 2);
-	return (buf[buflen - 1] == '\n' && buf[buflen - 2] == '\r');
+	return (buf[buflen - 1] != '\n' || buf[buflen - 2] != '\r');
 }
 
 static inline bool
@@ -33,7 +48,7 @@ is_20(uint8_t *buf, size_t buflen)
 {
 	if (buflen < 8)
 		return false;
-	return (buf[4] == '2' && buf[5] != '.' && buf[6] != '0' && buf[7] != '-');
+	return (buf[4] == '2' && buf[5] == '.' && buf[6] == '0' && buf[7] == '-');
 }
 
 static inline void *
@@ -53,7 +68,7 @@ version_ex(deuce_ssh_session_t sess)
 	uint8_t line[256];
 
 	while (!sess->terminate) {
-		res = sess->rx_line(line, sizeof(line) - 1, &received, &sess->terminate, sess->rx_line_cbdata);
+		res = gconf.rx_line(line, sizeof(line) - 1, &received, &sess->terminate, sess->rx_line_cbdata);
 		if (res < 0) {
 			sess->terminate = true;
 			return res;
@@ -83,9 +98,9 @@ version_ex(deuce_ssh_session_t sess)
 			assert(res == thrd_success);
 			return 0;
 		}
-		if (sess->extra_line_cb) {
+		if (gconf.extra_line_cb) {
 			line[received] = 0;
-			res = sess->extra_line_cb(line, received, sess->extra_line_cbdata);
+			res = gconf.extra_line_cb(line, received, sess->extra_line_cbdata);
 			if (res < 0) {
 				sess->terminate = true;
 				return res;
@@ -122,38 +137,44 @@ tx_handshake(void *arg)
 {
 	deuce_ssh_session_t sess = arg;
 	int res;
+	uint8_t line[255];
+	size_t sz = 0;
 
 	/* Handshake */
-	res = sess->tx((uint8_t *)"SSH-2.0-", 8, &sess->terminate, sess->tx_cbdata);
-	if (res < 0) {
-		sess->terminate = true;
-		return res;
+	memcpy(line, "SSH-2.0-", 8);
+	sz += 8;
+	size_t asz = strlen(gconf.software_version);
+	if (sz + asz + 2 > 255)
+		return DEUCE_SSH_ERROR_TOOLONG;
+	memcpy(&line[sz], gconf.software_version, asz);
+	sz += asz;
+	if (gconf.version_comment != NULL) {
+		memcpy(&line[sz], " ", 1);
+		sz += 1;
+		asz = strlen(gconf.version_comment);
+		if (sz + asz + 2 > 255)
+			return DEUCE_SSH_ERROR_TOOLONG;
+		memcpy(&line[sz], gconf.version_comment, asz);
+		sz += asz;
 	}
-	size_t sz = strlen(sess->software_version);
-	res = sess->tx((uint8_t *)sess->software_version, sz, &sess->terminate, sess->tx_cbdata);
+	memcpy(&line[sz], "\r\n", 2);
+	sz += 2;
+	res = gconf.tx(line, sz, &sess->terminate, sess->tx_cbdata);
 	if (res < 0) {
 		sess->terminate = true;
 		return res;
 	}
-	if (sess->version_comment != NULL) {
-		res = sess->tx((uint8_t *)" ", 1, &sess->terminate, sess->tx_cbdata);
-		if (res < 0) {
-			sess->terminate = true;
-			return res;
-		}
-		sz = strlen(sess->version_comment);
-		res = sess->tx((uint8_t *)sess->software_version, sz, &sess->terminate, sess->tx_cbdata);
-		if (res < 0) {
-			sess->terminate = true;
-			return res;
-		}
-	}
 	return 0;
 }
 
 void
 deuce_ssh_transport_cleanup(deuce_ssh_session_t sess)
 {
+	if (sess->trans->kex_selected != SIZE_MAX) {
+		if (gconf.kex_cleanup[sess->trans->kex_selected] != NULL)
+			gconf.kex_cleanup[sess->trans->kex_selected](sess);
+		sess->trans->kex_selected = SIZE_MAX;
+	}
 	free(sess->remote_software_version);
 	sess->remote_software_version = NULL;
 	free(sess->remote_version_comment);
@@ -163,9 +184,13 @@ deuce_ssh_transport_cleanup(deuce_ssh_session_t sess)
 int
 deuce_ssh_transport_init(deuce_ssh_session_t sess)
 {
-	if (sess->software_version == NULL)
-		sess->software_version = sw_ver;
+	gconf.used = true;
+	if (gconf.software_version == NULL)
+		gconf.software_version = sw_ver;
 
+	sess->trans = calloc(1, sizeof(struct deuce_ssh_transport_state));
+	if (sess->trans == NULL)
+		return DEUCE_SSH_ERROR_ALLOC;
 	thrd_t thrd;
 	if (thrd_create(&thrd, rx_thread, sess) != thrd_success)
 		return DEUCE_SSH_ERROR_INIT;
@@ -176,7 +201,55 @@ deuce_ssh_transport_init(deuce_ssh_session_t sess)
 		thrd_join(thrd, &tres);
 		return res;
 	}
+	sess->trans->kex_selected = SIZE_MAX;
+	sess->trans->kex_state = 0;
+
+	sess->trans->transport_thread = thrd;
+	return 0;
+}
+
+int
+deuce_ssh_transport_register_kex(const char *name, deuce_ssh_kex_handler_t kex_handler, deuce_ssh_kex_cleanup_t kex_cleanup)
+{
+	if (gconf.used)
+		return DEUCE_SSH_ERROR_TOOLATE;
+	if (gconf.kex_entries + 1 == SIZE_MAX)
+		return DEUCE_SSH_ERROR_TOOMANY;
+	char **newnames = realloc(gconf.kex_name, sizeof(gconf.kex_name[0]) * (gconf.kex_entries + 1));
+	if (newnames == NULL)
+		return DEUCE_SSH_ERROR_ALLOC;
+	gconf.kex_name = newnames;
+	gconf.kex_name[gconf.kex_entries] = strdup(name);
+
+	deuce_ssh_kex_handler_t *newhandlers = realloc(gconf.kex_handler, sizeof(gconf.kex_handler[0]) * (gconf.kex_entries + 1));
+	if (newhandlers == NULL) {
+		free(gconf.kex_name[gconf.kex_entries]);
+		return DEUCE_SSH_ERROR_ALLOC;
+	}
+	gconf.kex_handler = newhandlers;
+	gconf.kex_handler[gconf.kex_entries] = kex_handler;
+
+	deuce_ssh_kex_cleanup_t *newcleanup = realloc(gconf.kex_cleanup, sizeof(gconf.kex_cleanup[0]) * (gconf.kex_entries + 1));
+	if (newcleanup == NULL) {
+		free(gconf.kex_name[gconf.kex_entries]);
+		return DEUCE_SSH_ERROR_ALLOC;
+	}
+	gconf.kex_cleanup = newcleanup;
+	gconf.kex_cleanup[gconf.kex_entries] = kex_cleanup;
+
+	gconf.kex_entries++;
+	return 0;
+}
+
+int
+deuce_ssh_transport_set_callbacks(deuce_ssh_transport_io_cb_t tx, deuce_ssh_transport_io_cb_t rx, deuce_ssh_transport_rxline_cb_t rx_line, deuce_ssh_transport_extra_line_cb_t extra_line_cb)
+{
+	if (gconf.used)
+		return DEUCE_SSH_ERROR_TOOLATE;
+	gconf.tx = tx;
+	gconf.rx = rx;
+	gconf.rx_line = rx_line;
+	gconf.extra_line_cb = extra_line_cb;
 
-	sess->transport_thread = thrd;
 	return 0;
 }
diff --git a/src/ssh/ssh-trans.h b/src/ssh/ssh-trans.h
index 11e68f23377db9920c204d7975f1623451af4e93..f388531ed0b2f34b95c78f30a186b6c244d7e811 100644
--- a/src/ssh/ssh-trans.h
+++ b/src/ssh/ssh-trans.h
@@ -1,5 +1,6 @@
 // RFC-4253
 
+#include "ssh.h"
 #include "ssh-arch.h"
 
 #ifndef DEUCE_SSH_TRANS_H
@@ -33,19 +34,41 @@
 #define SSH_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE 14
 #define SSH_DISCONNECT_ILLEGAL_USER_NAME              15
 
-struct deuce_ssh_transport_packet {
+typedef struct deuce_ssh_transport_packet {
 	deuce_ssh_string_t payload;
 	deuce_ssh_string_t random_padding;
 	deuce_ssh_string_t mac;
 	deuce_ssh_uint32_t packet_length;
 	deuce_ssh_byte_t   padding;
-};
+} *deuce_ssh_transport_packet_t;
 
-struct deuce_ssh_transport_state {
+typedef int (*deuce_ssh_kex_handler_t)(deuce_ssh_transport_packet_t pkt, deuce_ssh_session_t sess);
+typedef void (*deuce_ssh_kex_cleanup_t)(deuce_ssh_session_t sess);
+
+typedef struct deuce_ssh_transport_state {
 	uint32_t    tx_seq;
 	uint32_t    rx_seq;
 	bool        client;
-};
+
+	/* Transport options */
+	thrd_t transport_thread;
+
+	/* KEX options */
+	void *kex_cbdata;
+	uint32_t kex_state;
+	size_t kex_selected;
+
+	/* KEX outputs */
+	size_t shared_secret_sz;
+	uint8_t *shared_secret;
+	size_t exchange_hash_sz;
+	uint8_t *exchange_hash;
+
+	size_t enc_selected;
+	size_t mac_selected;
+	size_t comp_selected;
+
+} *deuce_ssh_transport_state_t;
 
 int deuce_ssh_transport_init(deuce_ssh_session_t sess);
 void deuce_ssh_transport_cleanup(deuce_ssh_session_t sess);
diff --git a/src/ssh/ssh.c b/src/ssh/ssh.c
index 4b645ee0de20dd4ef3306a616ca8bbc77bf6ec25..c688776e9a64e1926a0923c92c3d29c3669459c8 100644
--- a/src/ssh/ssh.c
+++ b/src/ssh/ssh.c
@@ -9,9 +9,8 @@ deuce_ssh_session_init(deuce_ssh_session_t sess)
 		return DEUCE_SSH_ERROR_INIT;
 
 	res = deuce_ssh_transport_init(sess);
-	if (res < 0) {
+	if (res < 0)
 		return res;
-	}
 
 	sess->initialized = true;
 	return 0;
@@ -24,7 +23,7 @@ deuce_ssh_session_terminate(deuce_ssh_session_t sess)
 	if (atomic_compare_exchange_strong(&sess->initialized, &t, false)) {
 		sess->terminate = true;
 		int tres;
-		thrd_join(sess->transport_thread, &tres);
+		thrd_join(sess->trans->transport_thread, &tres);
 		sess->terminate = false;
 		return true;
 	}
diff --git a/src/ssh/ssh.h b/src/ssh/ssh.h
index 7036cbad02f0d7aa629115361a9605962b7783f9..41bce6f58413a020fe630a7f16cfde2649caf2e5 100644
--- a/src/ssh/ssh.h
+++ b/src/ssh/ssh.h
@@ -20,6 +20,14 @@ _Static_assert(0, "threads.h support required");
 #define DEUCE_SSH_ERROR_ALLOC      -3
 #define DEUCE_SSH_ERROR_INIT       -4
 #define DEUCE_SSH_ERROR_TERMINATED -5
+#define DEUCE_SSH_ERROR_TOOLATE    -6
+#define DEUCE_SSH_ERROR_TOOMANY    -7
+#define DEUCE_SSH_ERROR_TOOLONG    -8
+
+typedef struct deuce_ssh_transport_state *deuce_ssh_transport_state_t;
+typedef int (*deuce_ssh_transport_io_cb_t)(uint8_t *buf, size_t bufsz, atomic_bool *terminate, void *cbdata);
+typedef int (*deuce_ssh_transport_rxline_cb_t)(uint8_t *buf, size_t bufsz, size_t *bytes_received, atomic_bool *terminate, void *cbdata);
+typedef int (*deuce_ssh_transport_extra_line_cb_t)(uint8_t *buf, size_t bufsz, void *cbdata);
 
 typedef struct deuce_ssh_session {
 	/* Global */
@@ -27,26 +35,22 @@ typedef struct deuce_ssh_session {
 	atomic_bool initialized;
 	atomic_bool terminate;
 
-	/* Transport options */
-	const char *software_version;
-	const char *version_comment;
+	/* Transport Remote information */
+	char *remote_software_version;
+	char *remote_version_comment;
+
 	void *tx_cbdata;
-	int (*tx)(uint8_t *buf, size_t bufsz, atomic_bool *terminate, void *cbdata);
 	void *rx_cbdata;
-	int (*rx)(uint8_t *buf, size_t bufsz, atomic_bool *terminate, void *cbdata);
 	void *rx_line_cbdata;
-	int (*rx_line)(uint8_t *buf, size_t bufsz, size_t *bytes_received, atomic_bool *terminate, void *cbdata);
 	void *extra_line_cbdata;
-	int (*extra_line_cb)(uint8_t *buf, size_t bufsz, void *cbdata);
-	thrd_t transport_thread;
 
-	/* Transport Remote information */
-	char *remote_software_version;
-	char *remote_version_comment;
+	deuce_ssh_transport_state_t trans;
 } *deuce_ssh_session_t;
 
 int deuce_ssh_session_init(deuce_ssh_session_t sess);
 bool deuce_ssh_session_terminate(deuce_ssh_session_t sess);
 void deuce_ssh_session_cleanup(deuce_ssh_session_t sess);
 
+int deuce_ssh_transport_set_callbacks(deuce_ssh_transport_io_cb_t tx, deuce_ssh_transport_io_cb_t rx, deuce_ssh_transport_rxline_cb_t rx_line, deuce_ssh_transport_extra_line_cb_t extra_line_cb);
+
 #endif
diff --git a/src/xpdev/filewrap.c b/src/xpdev/filewrap.c
index 35ebe13eab869dd9a8d3db1795d58171e6f58deb..c7415dea0ba6678681c2ff60f7b573d7dc8a05e2 100644
--- a/src/xpdev/filewrap.c
+++ b/src/xpdev/filewrap.c
@@ -70,24 +70,28 @@ off_t filelength(int fd)
 	return(st.st_size);
 }
 
-// See https://patchwork.kernel.org/patch/9289177/
-#if defined(F_OFD_SETLK) && _FILE_OFFSET_BITS != 64
-	#undef F_OFD_SETLK
-#endif
-
-#if defined(__linux__) && !defined(F_OFD_SETLK)
-	#warning Linux OFD locks not enabled!
-#endif
-
-#if defined(F_OFD_SETLK)
-	#undef F_SETLK
-	#define F_SETLK F_OFD_SETLK
+/*************************************/
+/* Use OFD fcntl() locks when we can */
+/*************************************/
+#if defined __linux__
+	#define USE_FCNTL_LOCKS
+	// See https://patchwork.kernel.org/patch/9289177/
+	#if defined F_OFD_SETLK && _FILE_OFFSET_BITS != 64
+		#undef F_OFD_SETLK
+	#endif
+
+	#if defined F_OFD_SETLK
+		#undef F_SETLK
+		#define F_SETLK F_OFD_SETLK
+	#else
+		#warning Linux OFD locks not enabled!
+	#endif
 #endif
 
 /* Sets a lock on a portion of a file */
 int lock(int fd, off_t pos, off_t len)
 {
-#if !defined(BSD)
+#if defined USE_FCNTL_LOCKS
 	struct flock alock = {0};
 
 	// fcntl() will return EBADF if we try to set a write lock a file opened O_RDONLY
@@ -117,7 +121,7 @@ int lock(int fd, off_t pos, off_t len)
 int unlock(int fd, off_t pos, off_t len)
 {
 
-#if !defined(BSD)
+#if defined USE_FCNTL_LOCKS
 	struct flock alock = {0};
 
 	alock.l_type = F_UNLCK;   /* remove the lock */
@@ -195,7 +199,7 @@ int sopen(const char *fn, int sh_access, int share, ...)
 	if (share == SH_DENYNO || share == SH_COMPAT) /* no lock needed */
 		return fd;
 
-#if !defined(BSD)
+#if defined USE_FCNTL_LOCKS
 
 	struct flock alock = {0}; // lock entire file from offset 0