diff --git a/src/vdmodem/vdmodem.c b/src/vdmodem/vdmodem.c
new file mode 100644
index 0000000000000000000000000000000000000000..d3abc8fd40e5626fedac5e41afcb69288d1b98cd
--- /dev/null
+++ b/src/vdmodem/vdmodem.c
@@ -0,0 +1,969 @@
+/* Synchronet Virtual DOS Modem for Windows */
+
+/****************************************************************************
+ * @format.tab-size 4		(Plain Text/Source Code File Header)			*
+ * @format.use-tabs true	(see http://www.synchro.net/ptsc_hdr.html)		*
+ *																			*
+ * Copyright Rob Swindell - http://www.synchro.net/copyright.html			*
+ *																			*
+ * This program is free software; you can redistribute it and/or			*
+ * modify it under the terms of the GNU General Public License				*
+ * as published by the Free Software Foundation; either version 2			*
+ * of the License, or (at your option) any later version.					*
+ * See the GNU General Public License for more details: gpl.txt or			*
+ * http://www.fsf.org/copyleft/gpl.html										*
+ *																			*
+ * For Synchronet coding style and modification guidelines, see				*
+ * http://www.synchro.net/source.html										*
+ *																			*
+ * Note: If this box doesn't appear square, then you need to fix your tabs.	*
+ ****************************************************************************/
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdbool.h>
+#include <time.h>
+#define WIN32_LEAN_AND_MEAN
+#define NOGDI
+#include <windows.h>
+#include <process.h>
+
+#include "genwrap.h"
+#include "gen_defs.h"
+#include "sockwrap.h"
+
+#define TITLE "Synchronet Virtual DOS Modem for Windows"
+#define VERSION "0.0"
+
+SOCKET sock = INVALID_SOCKET;
+SOCKET listening_sock = INVALID_SOCKET;
+HANDLE hangup_event = INVALID_HANDLE_VALUE;	// e.g. program drops DTR
+HANDLE hungup_event = INVALID_HANDLE_VALUE;	// e.g. ATH0
+HANDLE carrier_event = INVALID_HANDLE_VALUE;
+HANDLE rdslot = INVALID_HANDLE_VALUE;
+HANDLE wrslot = INVALID_HANDLE_VALUE;
+union xp_sockaddr listening_interface;
+
+#define XTRN_IO_BUF_LEN 10000
+#define RING_DELAY 6000 /* US standard is 6 seconds */
+
+struct {
+	int node_num;
+	bool listen;
+	bool debug;
+	bool terminate_on_disconnect;
+	ulong data_rate;
+	enum {
+		 ADDRESS_FAMILY_UNSPEC
+		,ADDRESS_FAMILY_INET
+		,ADDRESS_FAMILY_INET6
+	} address_family;
+} cfg;
+
+static void dprintf(const char *fmt, ...)
+{
+	char buf[1024] = "SBBSVDM: ";
+	va_list argptr;
+
+    va_start(argptr,fmt);
+	size_t offset = strlen(buf);
+    _vsnprintf(buf + offset, sizeof(buf) - offset, fmt, argptr);
+	TERMINATE(buf);
+    va_end(argptr);
+    OutputDebugString(buf);
+}
+
+void usage(void)
+{
+	fprintf(stderr, "usage:\n");
+	exit(EXIT_SUCCESS);
+}
+
+const char* supported_cmds = "ADEHIMOQSVXZ&";
+const char* string_cmds = "D";
+struct modem {
+	enum {
+		 INIT
+		,A
+		,AT
+	} cmdstate;
+	char cr;
+	char lf;
+	char bs;
+	char esc;
+	bool echo_off;
+	bool numeric_mode;
+	bool offhook;
+	bool online; // false means "command mode"
+	bool ringing;
+	ulong ringcount;
+	ulong auto_answer;
+	ulong dial_wait;
+	ulong guard_time;
+	ulong esc_count;
+	ulong ext_results;
+	ulong quiet;
+	uint8_t buf[128];
+	size_t buflen;
+};
+
+void newcmd(struct modem* modem)
+{
+	modem->cmdstate = INIT;
+	modem->buflen = 0;
+}
+
+void init(struct modem* modem)
+{
+	memset(modem, 0, sizeof(*modem));
+	modem->cr = '\r';
+	modem->lf = '\n';
+	modem->bs = '\b';
+	modem->esc = '+';
+	modem->ext_results = 4;
+	modem->dial_wait = 60;
+	modem->guard_time = 50;
+}
+
+ulong guard_time(struct modem* modem)
+{
+	return modem->guard_time * 20;
+}
+
+ulong count_esc(struct modem* modem, uint8_t* buf, size_t rd)
+{
+	if(modem->esc < 0 || modem->esc > 127)
+		return 0;
+
+	ulong count = 0;
+	for(size_t i = 0; i < rd; i++) {
+		if(buf[i] == modem->esc)
+			count++;
+	}
+	return count;
+}
+
+// Basic Hayes modem responses
+enum modem_response {
+	OK				= 0,
+	CONNECT			= 1,
+	RING			= 2,
+	NO_CARRIER		= 3,
+	ERROR			= 4,
+	CONNECT_1200	= 5,
+	NO_DIAL_TONE	= 6,
+	BUSY			= 7,
+	NO_ANSWER		= 8,
+	RESERVED1		= 9,
+	CONNECT_2400	= 10,
+	RESERVED2		= 11,
+	RESERVED3		= 12,
+	CONNECT_9600	= 13
+};
+
+const char* response_str[] = {
+	"OK",
+	"CONNECT",
+	"RING",
+	"NO CARRIER",
+	"ERROR",
+	"CONNECT 1200",
+	"NO DIAL TONE",
+	"BUSY",
+	"NO ANSWER",
+	"RESERVED1",
+	"CONNECT 2400",
+	"RESERVED2",
+	"RESERVED3",
+	"CONNECT 9600"
+};
+
+char* response(struct modem* modem, enum modem_response code)
+{
+	static char str[128];
+
+	if(modem->quiet)
+		return "";
+	if(modem->numeric_mode)
+		sprintf(str, "%u%c", code, modem->cr);
+	else
+		sprintf(str, "%c%c%s%c%c", modem->cr, modem->lf, response_str[code], modem->cr, modem->lf);
+	return str;
+}
+
+char* ok(struct modem* modem)
+{
+	return response(modem, OK);
+}
+
+char* error(struct modem* modem)
+{
+	return response(modem, ERROR);
+}
+
+char* connect_result(struct modem* modem)
+{
+	return response(modem, modem->ext_results ? CONNECT_9600 : CONNECT);
+}
+
+char* connected(struct modem* modem)
+{
+	modem->online = true;
+	modem->ringing = false;
+	ResetEvent(hungup_event);
+	SetEvent(carrier_event);
+	return connect_result(modem);
+}
+
+void disconnect(struct modem* modem)
+{
+	modem->online = false;
+	shutdown(sock, SD_SEND);
+	closesocket(sock);
+	sock = INVALID_SOCKET;
+	SetEvent(hungup_event);
+	SetEvent(carrier_event);
+}
+
+bool kbhit()
+{
+	unsigned long waiting = 0;
+	if(!GetMailslotInfo(
+		rdslot,				// mailslot handle
+ 		NULL,				// address of maximum message size
+		NULL,				// address of size of next message
+		&waiting,			// address of number of messages
+ 		NULL				// address of read time-out
+		))
+		return false;
+	return waiting != 0;
+}
+
+// Significant portions copies from syncterm/conn.c
+char* dial(struct modem* modem, const char* number)
+{
+	struct addrinfo	hints;
+	struct addrinfo	*res=NULL;
+	char host[128];
+	char* portnum = "23";
+
+	SAFECOPY(host, number);
+	char* p = strrchr(host, ':');
+	char* b = strrchr(host, ']'); 
+	if(p != NULL && p > b) {
+		portnum = p + 1;
+		*p = 0;
+	}
+	dprintf("Connecting to host '%s', port: %u", host, portnum);
+	memset(&hints, 0, sizeof(hints));
+	hints.ai_flags=PF_UNSPEC;
+	switch(cfg.address_family) {
+		case ADDRESS_FAMILY_INET:
+			hints.ai_family=PF_INET;
+			break;
+		case ADDRESS_FAMILY_INET6:
+			hints.ai_family=PF_INET6;
+			break;
+		case ADDRESS_FAMILY_UNSPEC:
+		default:
+			hints.ai_family=PF_UNSPEC;
+			break;
+	}
+	hints.ai_socktype=SOCK_STREAM;
+	hints.ai_protocol=IPPROTO_TCP;
+	hints.ai_flags=AI_NUMERICSERV;
+#ifdef AI_ADDRCONFIG
+	hints.ai_flags|=AI_ADDRCONFIG;
+#endif
+	dprintf("%s %d calling getaddrinfo", __FILE__, __LINE__);
+	int result = getaddrinfo(host, portnum, &hints, &res);
+	if(result != 0) {
+		dprintf("getaddrinfo(%s, %s) returned %d", host, portnum, result);
+		return response(modem, NO_ANSWER);
+	}
+
+	int				nonblock;
+	struct addrinfo	*cur;
+	for(cur=res; cur && sock == INVALID_SOCKET; cur=cur->ai_next) {
+		if(sock==INVALID_SOCKET) {
+			sock=socket(cur->ai_family, cur->ai_socktype, cur->ai_protocol);
+			if(sock==INVALID_SOCKET) {
+				dprintf("Error %ld creating socket", WSAGetLastError());
+				return response(modem, NO_DIAL_TONE);
+			}
+			/* Set to non-blocking for the connect */
+			nonblock=-1;
+			ioctlsocket(sock, FIONBIO, &nonblock);
+		}
+
+		dprintf("%s %d calling connect", __FILE__, __LINE__);
+		if(connect(sock, cur->ai_addr, cur->ai_addrlen)) {
+			switch(ERROR_VALUE) {
+				case EINPROGRESS:
+				case EINTR:
+				case EAGAIN:
+				case EWOULDBLOCK:
+					for(;sock!=INVALID_SOCKET;) {
+						if (socket_writable(sock, 1000)) {
+							if (socket_recvdone(sock, 0)) {
+								closesocket(sock);
+								sock=INVALID_SOCKET;
+								continue;
+							}
+							else {
+								goto connected;
+							}
+						}
+						else {
+							if (kbhit()) {
+								dprintf("%s %d kbhit", __FILE__, __LINE__);
+								closesocket(sock);
+								sock = INVALID_SOCKET;
+								return response(modem, NO_CARRIER);
+							}
+						}
+					}
+
+connected:
+					break;
+				default:
+					closesocket(sock);
+					sock=INVALID_SOCKET;
+					continue;
+			}
+		}
+	}
+	if (sock == INVALID_SOCKET) {
+		dprintf("%s %d invalid hostname?", __FILE__, __LINE__);
+		return response(modem, NO_ANSWER);
+	}
+
+	freeaddrinfo(res);
+	res=NULL;
+	nonblock=0;
+	ioctlsocket(sock, FIONBIO, &nonblock);
+	if(socket_recvdone(sock, 0)) {
+		dprintf("%s %d socket_recvdone", __FILE__, __LINE__);
+		return response(modem, NO_CARRIER);
+	}
+
+	dprintf("%s %d connected!", __FILE__, __LINE__);
+	int keepalives = TRUE;
+	setsockopt(sock, SOL_SOCKET, SO_KEEPALIVE, (void*)&keepalives, sizeof(keepalives));
+	return connected(modem);
+}
+
+char* answer(struct modem* modem)
+{
+	if(listening_sock == INVALID_SOCKET)
+		return response(modem, NO_DIAL_TONE);
+
+	fd_set fds = {0};
+	FD_SET(listening_sock, &fds);
+	struct timeval tv = { 0, 0 };
+	if(select(/* ignored: */0, &fds, NULL, NULL, &tv) != 1)
+		return response(modem, NO_CARRIER);
+
+	union xp_sockaddr addr;
+	socklen_t addrlen = sizeof(addr);
+	sock = accept(listening_sock, (SOCKADDR*)&addr, &addrlen);
+	if(sock == INVALID_SOCKET) {
+		dprintf("accept returned %d (errno=%ld)", sock, WSAGetLastError());
+		return response(modem, NO_CARRIER);
+	}
+	char tmp[256];
+	dprintf("Connection accepted from TCP port %hu at %s", inet_addrport(&addr), inet_addrtop(&addr, tmp, sizeof(tmp)));
+	return connected(modem);
+}
+
+char* atmodem_exec(struct modem* modem)
+{
+	static char respbuf[128];
+	char* resp = ok(modem);
+	modem->buf[modem->buflen] = '\0';
+	for(char* p = modem->buf; *p != '\0';) {
+		char ch = toupper(*p);
+		p++;
+		if(strchr(supported_cmds, ch) == NULL)
+			return error(modem);
+		if(strchr(string_cmds, ch) == NULL) {
+			if(ch == '&') {
+				p++;
+				ch = toupper(*p); // unused
+				ulong val = strtoul(p, &p, 10); // unused
+				continue;
+			}
+			// Numeric argument commands
+			ulong val = strtoul(p, &p, 10);
+			switch(ch) {
+				case 'A':
+					return answer(modem);
+				case 'E':
+					modem->echo_off = !val;
+					break;
+				case 'H':
+					modem->offhook = val;
+					modem->ringing = false;
+					if(!modem->offhook) {
+						if(sock != INVALID_SOCKET) {
+							disconnect(modem);
+						}
+					}
+					break;
+				case 'I':
+					sprintf(respbuf, "\r\n" TITLE " v" VERSION " Copyright %s Rob Swindell\r\n", &__DATE__[7]);
+					return respbuf;
+				case 'O':
+					if(sock == INVALID_SOCKET)
+						return error(modem);
+					modem->online = true;
+					return connect_result(modem);
+					break;
+				case 'V':
+					modem->numeric_mode = !val;
+					resp = ok(modem); // Use the new verbal/numeric mode in response (ala USRobotics)
+					break;
+				case 'Q':
+					modem->quiet = val;
+					resp = ok(modem); // Use the new quiet/verbose mode in response (ala USRobotics)
+					break;
+				case 'S':
+					if(*p == '=') {
+						ulong sreg = val;
+						ulong val = strtoul(p + 1, &p, 10);
+						dprintf("S%lu = %lu", sreg, val);
+						switch(sreg) {
+							case 0:
+								if(val && listening_sock == INVALID_SOCKET)
+									return error(modem);
+								modem->auto_answer = val;
+								break;
+							case 1:
+								modem->ringcount = val;
+								break;
+							case 2:
+								modem->esc = (char)val;
+								break;
+							case 3:
+								modem->cr = (char)val;
+								break;
+							case 4:
+								modem->lf = (char)val;
+								break;
+							case 5:
+								modem->bs = (char)val;
+								break;
+							case 7:
+								modem->dial_wait = val;
+								break;
+							case 12:
+								modem->guard_time = val;
+								break;
+						}
+					} else if(*p == '?') {
+						switch(val) {
+							case 0:
+								val = modem->auto_answer;
+								break;
+							case 1:
+								val = modem->ringcount;
+								break;
+							case 2:
+								val = modem->esc;
+								break;
+							case 3:
+								val = modem->cr;
+								break;
+							case 4:
+								val = modem->lf;
+								break;
+							case 5:
+								val = modem->bs;
+								break;
+							case 7:
+								val = modem->dial_wait;
+								break;
+							case 12:
+								val = modem->guard_time;
+								break;
+							default:
+								val = 0;
+								break;
+						}
+						sprintf(respbuf, "%c%03lu%c%c%s", modem->lf, val, modem->cr, modem->lf, ok(modem));
+						return respbuf;
+					} else
+						return error(modem);
+					break;
+				case 'X':
+					modem->ext_results = val;
+					break;
+				case 'Z':
+					init(modem);
+					break;
+			}
+		} else { // string argument commands
+			switch(ch) {
+				case 'D':
+					if(sock != INVALID_SOCKET)
+						return error(modem);
+					if(*p == 'T' /* tone */|| *p == 'P' /* pulse */)
+						p++;
+					return dial(modem, p);
+			}
+		}
+	}
+	return resp;
+}
+
+char* atmodem_parsech(struct modem* modem, uint8_t ch)
+{
+	switch(modem->cmdstate) {
+		case INIT:
+			if(toupper(ch) == 'A')
+				modem->cmdstate = A;
+			break;
+		case A:
+			if(toupper(ch) == 'T')
+				modem->cmdstate = AT;
+			else
+				newcmd(modem);
+			break;
+		case AT:
+			if(ch == modem->cr) {
+				char* retval = atmodem_exec(modem);
+				newcmd(modem);
+				return retval;
+			} else if(ch == modem->bs) {
+				if(modem->buflen)
+					modem->buflen--;
+			} else {
+				if(modem->buflen >= sizeof(modem->buf))
+					return error(modem);
+				if(ch != ' ')
+					modem->buf[modem->buflen++] = ch;
+			}
+			break;
+	}
+	return NULL;
+}
+
+char* atmodem_parse(struct modem* modem, uint8_t* buf, size_t len)
+{
+	for(size_t i = 0; i < len; i++) {
+		char* resp = atmodem_parsech(modem, buf[i]);
+		if(resp != NULL)
+			return resp;
+	}
+	return NULL;
+}
+
+BOOL vdd_write(HANDLE* slot, uint8_t* buf, size_t buflen)
+{
+	if(*slot == INVALID_HANDLE_VALUE) {
+		char path[MAX_PATH + 1];
+		sprintf(path, "\\\\.\\mailslot\\sbbsexec\\wr%d", cfg.node_num);
+		*slot = CreateFile(path
+			,GENERIC_WRITE
+			,FILE_SHARE_READ
+			,NULL
+			,OPEN_EXISTING
+			,FILE_ATTRIBUTE_NORMAL
+			,(HANDLE) NULL);
+		if(*slot == INVALID_HANDLE_VALUE) {
+			dprintf("!ERROR %u (%s) opening '%s'", GetLastError(), strerror(errno), path);
+			exit(1);
+		}
+	}
+	DWORD wr = 0;
+	BOOL result = WriteFile(*slot, buf, buflen, &wr, /* LPOVERLAPPED */NULL);
+	if(wr != buflen)
+		dprintf("WriteFile wrote %ld instead of %ld", wr, buflen);
+	return result;
+}
+
+BOOL vdd_writestr(HANDLE* slot, char* str)
+{
+	return vdd_write(slot, str, strlen(str));
+}
+
+void listen_thread(void* arg)
+{
+	struct modem* modem = (struct modem*)arg;
+
+	for(;;) {
+		fd_set fds = {0};
+		FD_SET(listening_sock, &fds);
+		struct timeval tv = { 1, 0 };
+		if(select(/* ignored: */0, &fds, NULL, NULL, &tv) == 1) {
+			if(sock != INVALID_SOCKET) {	// In-use
+				SOCKADDR_IN addr;
+				socklen_t addrlen = sizeof(addr);
+				SOCKET newsock = accept(listening_sock, (SOCKADDR*)&addr, &addrlen);
+				if(newsock != INVALID_SOCKET) {
+					char* busy_notice = "\r\nSorry, not available right now\r\n";
+					send(newsock, busy_notice, strlen(busy_notice), /* flags: */0);
+					shutdown(newsock, SD_SEND);
+					closesocket(newsock);
+				}
+				continue;
+			}
+			if(!modem->offhook && !modem->online)
+				modem->ringing = true;
+		}
+	}
+}
+
+int main(int argc, char** argv)
+{
+	int argn = 1;
+	char tmp[256];
+	char path[MAX_PATH + 1];
+	char fullmodemline[MAX_PATH + 1];
+	uint8_t buf[XTRN_IO_BUF_LEN];
+	size_t rx_buflen = sizeof(buf);
+	ULONGLONG rx_delay = 0;
+	WSADATA WSAData;
+	int	result;
+
+	fprintf(stderr, TITLE " v" VERSION " Copyright %s Rob Swindell\n", &__DATE__[7]);
+    if((result = WSAStartup(MAKEWORD(1,1), &WSAData)) == 0)
+		dprintf("%s %s",WSAData.szDescription, WSAData.szSystemStatus);
+	else {
+		fprintf(stderr,"!WinSock startup ERROR %d", result);
+		return EXIT_FAILURE;
+	}
+
+	listening_interface.addr.sa_family = cfg.address_family;
+
+	for(; argn < argc; argn++) {
+		char* arg = argv[argn];
+		if(*arg != '-')
+			break;
+		while(*arg == '-')
+			arg++;
+		switch(*arg) {
+			case '6':
+				listening_interface.addr.sa_family = AF_INET6;
+				break;
+			case 'l':
+				cfg.listen = true;
+				arg++;
+				if(*arg != '\0') {
+					if(inet_ptoaddr(arg, &listening_interface, sizeof(listening_interface)) == NULL) {
+						fprintf(stderr, "!Error parsing network address: %s", arg);
+						return EXIT_FAILURE;
+					}
+				}
+				break;
+			case 'p':
+				inet_setaddrport(&listening_interface, atoi(arg + 1));
+				break;
+			case 'd':
+				cfg.debug = true;
+				break;
+			case 'h':
+				sock = strtoul(arg + 1, NULL, 10);
+				break;
+			case 'r':
+				cfg.data_rate = strtoul(arg + 1, NULL, 10);
+				break;
+			case 'b':
+				rx_buflen = min(strtoul(arg + 1, NULL, 10), sizeof(buf));
+			case 'R':
+				rx_delay = strtoul(arg + 1, NULL, 10);
+				break;
+			default:
+				usage();
+				break;
+		}
+	}
+	if(argn >= argc) {
+		usage();
+	}
+
+	struct modem modem = {0};
+	init(&modem);
+
+	if(cfg.listen) {
+		if(sock != INVALID_SOCKET)
+			listening_sock = sock;
+		else {
+			listening_sock = socket(PF_INET, SOCK_STREAM, IPPROTO_IP);
+			if(listening_sock == INVALID_SOCKET) {
+				fprintf(stderr, "Error %ld creating socket\n", WSAGetLastError());
+				return EXIT_FAILURE;
+			}
+		}
+		result = bind(listening_sock, &listening_interface.addr, xp_sockaddr_len(&listening_interface));
+		if(result != 0) {
+			fprintf(stderr, "Error %d binding socket\n", WSAGetLastError());
+			return EXIT_FAILURE;
+		}
+		if(listen(listening_sock, /* backlog: */1) != 0) {
+			fprintf(stderr, "Error %d listening on socket\n", WSAGetLastError());
+			return EXIT_FAILURE;
+		}
+		fprintf(stderr, "Listening on TCP port %u at %s\n"
+			,inet_addrport(&listening_interface), inet_addrtop(&listening_interface, tmp, sizeof(tmp)));
+
+		_beginthread(listen_thread, /* stack_size: */0, &modem);
+	} else {
+		if(sock != INVALID_SOCKET)
+			connected(&modem);
+	}
+
+	const char* dropfile = "dosxtrn.env";
+	FILE* fp = fopen(dropfile, "w");
+	if(fp == NULL) {
+		perror(dropfile);
+		return EXIT_FAILURE;
+	}
+	char* dospgm = argv[argn];
+	for(; argn < argc; argn++) {
+		fprintf(fp, "%s ", argv[argn]);
+	}
+	fputc('\n', fp);
+	fclose(fp);
+
+	while(1) {
+		sprintf(path, "\\\\.\\mailslot\\sbbsexec\\rd%d", cfg.node_num);
+		rdslot = CreateMailslot(path
+			,sizeof(buf)/2			// Maximum message size (0=unlimited)
+			,0						// Read time-out
+			,NULL);                 // Security
+		if(rdslot!=INVALID_HANDLE_VALUE)
+			break;
+		if(cfg.node_num == 0xff) {
+			fprintf(stderr, "Error %ld creating '%s'\n", GetLastError(), path);
+			return EXIT_FAILURE;
+		}
+		++cfg.node_num;
+	}
+
+	sprintf(path, "sbbsexec_carrier%d", cfg.node_num);
+	carrier_event = CreateEvent(
+		 NULL	// pointer to security attributes
+		,FALSE	// flag for manual-reset event
+		,FALSE  // flag for initial state
+		,path	// pointer to event-object name
+		);
+	if(carrier_event == NULL) {
+		fprintf(stderr, "Error %ld creating '%s'\n", GetLastError(), path);
+		return EXIT_FAILURE;
+	}
+
+	sprintf(path, "sbbsexec_hangup%d", cfg.node_num);
+	hangup_event = CreateEvent(
+		 NULL	// pointer to security attributes
+		,FALSE	// flag for manual-reset event
+		,FALSE  // flag for initial state (DTR = high)
+		,path	// pointer to event-object name
+		);
+	if(hangup_event == NULL) {
+		fprintf(stderr, "Error %ld creating '%s'\n", GetLastError(), path);
+		return EXIT_FAILURE;
+	}
+
+	sprintf(path, "sbbsexec_hungup%d", cfg.node_num);
+	hungup_event = CreateEvent(
+		 NULL	// pointer to security attributes
+		,TRUE	// flag for manual-reset event
+		,TRUE   // flag for initial state (DCD = low)
+		,path	// pointer to event-object name
+		);
+	if(hungup_event == NULL) {
+		fprintf(stderr, "Error %ld creating '%s'\n", GetLastError(), path);
+		return EXIT_FAILURE;
+	}
+
+    STARTUPINFO startup_info={0};
+    startup_info.cb=sizeof(startup_info);
+
+	BOOL x64 = FALSE;
+	IsWow64Process(GetCurrentProcess(), &x64);
+	sprintf(fullmodemline, "dosxtrn.exe %s %s %u svdm.ini", dropfile, x64 ? "x64" : "NT", cfg.node_num);
+
+	PROCESS_INFORMATION process_info;
+    if(!CreateProcess(
+		NULL,			// pointer to name of executable module
+		fullmodemline,  	// pointer to command line string
+		NULL,  			// process security attributes
+		NULL,   		// thread security attributes
+		FALSE, 			// handle inheritance flag
+		0, //CREATE_NEW_CONSOLE/*|CREATE_SEPARATE_WOW_VDM*/, // creation flags
+        NULL,			// pointer to new environment block
+		NULL		,	// pointer to current directory name
+		&startup_info,  // pointer to STARTUPINFO
+		&process_info  	// pointer to PROCESS_INFORMATION
+		)) {
+        fprintf(stderr, "Error %ld executing '%s'", GetLastError(), fullmodemline);
+		return EXIT_FAILURE;
+	}
+	printf("Executed '%s' successfully\n", fullmodemline);
+
+	CloseHandle(process_info.hThread);
+
+	if(cfg.data_rate > 0) {
+		rx_buflen = max(cfg.data_rate / 100, 1);
+		rx_delay = (ULONGLONG) (1000 * ((double)rx_buflen / cfg.data_rate));
+	}
+
+	ULONGLONG lastring = 0;
+	ULONGLONG lasttx = 0;
+	ULONGLONG lastrx = 0;
+	int largest_recv = 0;
+
+	while(WaitForSingleObject(process_info.hProcess,0) != WAIT_OBJECT_0) {
+		ULONGLONG now = xp_timer64();
+		if(modem.online) {
+			fd_set fds = {0};
+			FD_SET(sock, &fds);
+			struct timeval tv = { 0, 0 };
+			if(now - lastrx >= rx_delay && select(/* ignored: */0, &fds, NULL, NULL, &tv) == 1) {
+				dprintf("select returned 1");
+				int rd = recv(sock, buf, rx_buflen, /* flags: */0);
+				dprintf("recv returned %d", rd);
+				if(rd <= 0) {
+					int error = WSAGetLastError();
+					if(rd == 0 || error == WSAECONNRESET) {
+						dprintf("Connection reset detected");
+						disconnect(&modem);
+						vdd_writestr(&wrslot, response(&modem, NO_CARRIER));
+						continue;
+					}
+					dprintf("Socket error %ld on recv", error);
+					continue;
+				}
+				if(rd > largest_recv)
+					largest_recv = rd;
+				vdd_write(&wrslot, buf, rd);
+				lastrx = now;
+			}
+			if(WaitForSingleObject(hangup_event, 0) == WAIT_OBJECT_0) {
+				dprintf("hangup_event signaled");
+				disconnect(&modem);
+				vdd_writestr(&wrslot, response(&modem, NO_CARRIER));
+			}
+		} else {
+			if(modem.ringing) {
+				if(modem.ringcount < 1)
+					dprintf("Incoming connection");
+				if(now - lastring > RING_DELAY) {
+					dprintf("RING");
+					vdd_writestr(&wrslot, response(&modem, RING));
+					lastring = now;
+					modem.ringcount++;
+					if(modem.auto_answer > 0 && modem.ringcount >= modem.auto_answer) {
+						vdd_writestr(&wrslot, answer(&modem));
+					}
+				}
+			}
+			if(cfg.terminate_on_disconnect) {
+				dprintf("Terminating process on disconnect");
+				TerminateProcess(process_info.hProcess, 2112);
+			}
+		}
+
+		size_t rd = 0;
+		size_t len = sizeof(buf);
+//		avail=RingBufFree(&outbuf)/2;	// leave room for telnet expansion
+//		if(len>avail)
+//            len=avail;
+
+		while(rd<len) {
+			unsigned long waiting = 0;
+			unsigned long msglen = 0;
+
+			GetMailslotInfo(
+				rdslot,				// mailslot handle
+ 				NULL,				// address of maximum message size
+				NULL,				// address of size of next message
+				&waiting,			// address of number of messages
+ 				NULL				// address of read time-out
+				);
+			if(!waiting)
+				break;
+			if(ReadFile(rdslot, buf+rd, len-rd, &msglen, NULL)==FALSE || msglen<1)
+				break;
+			rd+=msglen;
+		}
+		if(rd) {
+			if(modem.online) {
+				if(modem.esc_count) {
+					if(modem.esc_count >= 3)
+						modem.esc_count = 0;
+					else  {
+						if(now - lasttx < guard_time(&modem))
+							if(*buf == modem.esc)
+								modem.esc_count += count_esc(&modem, buf, rd);
+							else
+								modem.esc_count = 0;
+					}
+				} else {
+					if(now - lasttx > guard_time(&modem))
+						modem.esc_count = count_esc(&modem, buf, rd);
+				}
+				int wr = send(sock, buf, rd, /* flags: */0);
+				if(wr != rd)
+					dprintf("Sent %d instead of %d", wr, rd);
+				else if(cfg.debug)
+					dprintf("TX: %d bytes", wr);
+			} else { // Command mode
+				dprintf("RX command: '%.*s'\n", rd, buf);
+				if(!modem.echo_off)
+					vdd_write(&wrslot, buf, rd);
+				char* response = atmodem_parse(&modem, buf, rd);
+				if(response != NULL) {
+					vdd_writestr(&wrslot, response);
+					SKIP_WHITESPACE(response);
+					dprintf("Modem response: %s", response);
+				}
+			}
+			lasttx = now;
+		} else {
+			if(modem.online && modem.esc_count == 3 && now - lasttx >= guard_time(&modem)) {
+				dprintf("Entering command mode");
+				modem.online = false;
+				modem.esc_count = 0;
+				vdd_writestr(&wrslot, ok(&modem));
+			}
+		}
+	}
+
+	int retval = EXIT_SUCCESS;
+	fp = fopen("DOSXTRN.RET", "r");
+	if(fp == NULL) {
+		perror("DOSXTRN.RET");
+	} else {
+		if(fscanf(fp, "%d", &retval) != 1) {
+			fprintf(stderr, "Error reading return value from DOSXTRN.REG");
+			retval = EXIT_FAILURE;
+		}
+		fclose(fp);
+		if(retval == -1) {
+			fprintf(stderr, "DOSXTRN failed to execute '%s': ", dospgm);
+			fp = fopen("DOSXTRN.ERR", "r");
+			if(fp == NULL) {
+				perror("DOSXTRN.ERR");
+			} else {
+				char errstr[256] = "";
+				int errval = 0;
+				if(fscanf(fp, "%d\n", &errval) == 1) {
+					fgets(errstr, sizeof(errstr), fp);
+					truncsp(errstr);
+					fprintf(stderr, "Error %d (%s)\n", errval, errstr);
+				} else
+					fprintf(stderr, "Failed to parse DOSXTRN.ERR\n");
+				fclose(fp);
+			}
+		}
+	}
+
+	if(cfg.debug) {
+		printf("rx_delay: %lld\n", rx_delay);
+		printf("rx_buflen: %ld\n", rx_buflen);
+		printf("largest recv: %d\n", largest_recv);
+	}
+	return retval;
+}
diff --git a/src/vdmodem/vdmodem.sln b/src/vdmodem/vdmodem.sln
new file mode 100644
index 0000000000000000000000000000000000000000..ef3ad4cb388e576bbeb46d34a19a3708af3768e8
--- /dev/null
+++ b/src/vdmodem/vdmodem.sln
@@ -0,0 +1,25 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.32106.194
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "vdmodem", "vdmodem.vcxproj", "{20051597-6298-4098-8F26-E408C2880FE4}"
+EndProject
+Global
+	GlobalSection(SolutionConfigurationPlatforms) = preSolution
+		Debug|x86 = Debug|x86
+		Release|x86 = Release|x86
+	EndGlobalSection
+	GlobalSection(ProjectConfigurationPlatforms) = postSolution
+		{20051597-6298-4098-8F26-E408C2880FE4}.Debug|x86.ActiveCfg = Debug|Win32
+		{20051597-6298-4098-8F26-E408C2880FE4}.Debug|x86.Build.0 = Debug|Win32
+		{20051597-6298-4098-8F26-E408C2880FE4}.Release|x86.ActiveCfg = Release|Win32
+		{20051597-6298-4098-8F26-E408C2880FE4}.Release|x86.Build.0 = Release|Win32
+	EndGlobalSection
+	GlobalSection(SolutionProperties) = preSolution
+		HideSolutionNode = FALSE
+	EndGlobalSection
+	GlobalSection(ExtensibilityGlobals) = postSolution
+		SolutionGuid = {9F30FE81-9971-48D0-AC51-EE3F15248CB0}
+	EndGlobalSection
+EndGlobal
diff --git a/src/vdmodem/vdmodem.vcxproj b/src/vdmodem/vdmodem.vcxproj
new file mode 100644
index 0000000000000000000000000000000000000000..584f332ccf30b33cad25b020ec7cd7e3a8530c85
--- /dev/null
+++ b/src/vdmodem/vdmodem.vcxproj
@@ -0,0 +1,161 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <ItemGroup Label="ProjectConfigurations">
+    <ProjectConfiguration Include="Debug|Win32">
+      <Configuration>Debug</Configuration>
+      <Platform>Win32</Platform>
+    </ProjectConfiguration>
+    <ProjectConfiguration Include="Release|Win32">
+      <Configuration>Release</Configuration>
+      <Platform>Win32</Platform>
+    </ProjectConfiguration>
+    <ProjectConfiguration Include="Debug|x64">
+      <Configuration>Debug</Configuration>
+      <Platform>x64</Platform>
+    </ProjectConfiguration>
+    <ProjectConfiguration Include="Release|x64">
+      <Configuration>Release</Configuration>
+      <Platform>x64</Platform>
+    </ProjectConfiguration>
+  </ItemGroup>
+  <PropertyGroup Label="Globals">
+    <VCProjectVersion>16.0</VCProjectVersion>
+    <Keyword>Win32Proj</Keyword>
+    <ProjectGuid>{20051597-6298-4098-8f26-e408c2880fe4}</ProjectGuid>
+    <RootNamespace>svdmodem</RootNamespace>
+    <WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
+  </PropertyGroup>
+  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
+    <ConfigurationType>Application</ConfigurationType>
+    <UseDebugLibraries>true</UseDebugLibraries>
+    <PlatformToolset>v142</PlatformToolset>
+    <CharacterSet>NotSet</CharacterSet>
+  </PropertyGroup>
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
+    <ConfigurationType>Application</ConfigurationType>
+    <UseDebugLibraries>false</UseDebugLibraries>
+    <PlatformToolset>v142</PlatformToolset>
+    <WholeProgramOptimization>true</WholeProgramOptimization>
+    <CharacterSet>NotSet</CharacterSet>
+  </PropertyGroup>
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
+    <ConfigurationType>Application</ConfigurationType>
+    <UseDebugLibraries>true</UseDebugLibraries>
+    <PlatformToolset>v142</PlatformToolset>
+    <CharacterSet>NotSet</CharacterSet>
+  </PropertyGroup>
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
+    <ConfigurationType>Application</ConfigurationType>
+    <UseDebugLibraries>false</UseDebugLibraries>
+    <PlatformToolset>v142</PlatformToolset>
+    <WholeProgramOptimization>true</WholeProgramOptimization>
+    <CharacterSet>NotSet</CharacterSet>
+  </PropertyGroup>
+  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
+  <ImportGroup Label="ExtensionSettings">
+  </ImportGroup>
+  <ImportGroup Label="Shared">
+  </ImportGroup>
+  <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
+    <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
+    <Import Project="..\build\undeprecate.props" />
+    <Import Project="..\xpdev\xpdev_mt.props" />
+  </ImportGroup>
+  <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
+    <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
+    <Import Project="..\build\undeprecate.props" />
+    <Import Project="..\xpdev\xpdev_mt.props" />
+  </ImportGroup>
+  <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
+    <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
+    <Import Project="..\build\undeprecate.props" />
+    <Import Project="..\xpdev\xpdev_mt.props" />
+  </ImportGroup>
+  <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
+    <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
+    <Import Project="..\build\undeprecate.props" />
+    <Import Project="..\xpdev\xpdev_mt.props" />
+  </ImportGroup>
+  <PropertyGroup Label="UserMacros" />
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
+    <LinkIncremental>true</LinkIncremental>
+    <TargetName>svdm</TargetName>
+  </PropertyGroup>
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
+    <LinkIncremental>false</LinkIncremental>
+    <TargetName>svdm</TargetName>
+  </PropertyGroup>
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
+    <LinkIncremental>true</LinkIncremental>
+    <TargetName>svdm</TargetName>
+  </PropertyGroup>
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
+    <LinkIncremental>false</LinkIncremental>
+    <TargetName>svdm</TargetName>
+  </PropertyGroup>
+  <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
+    <ClCompile>
+      <WarningLevel>Level3</WarningLevel>
+      <SDLCheck>true</SDLCheck>
+      <PreprocessorDefinitions>WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
+      <ConformanceMode>true</ConformanceMode>
+    </ClCompile>
+    <Link>
+      <SubSystem>Console</SubSystem>
+      <GenerateDebugInformation>true</GenerateDebugInformation>
+    </Link>
+  </ItemDefinitionGroup>
+  <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
+    <ClCompile>
+      <WarningLevel>Level3</WarningLevel>
+      <FunctionLevelLinking>true</FunctionLevelLinking>
+      <IntrinsicFunctions>true</IntrinsicFunctions>
+      <SDLCheck>true</SDLCheck>
+      <PreprocessorDefinitions>WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
+      <ConformanceMode>true</ConformanceMode>
+    </ClCompile>
+    <Link>
+      <SubSystem>Console</SubSystem>
+      <EnableCOMDATFolding>true</EnableCOMDATFolding>
+      <OptimizeReferences>true</OptimizeReferences>
+      <GenerateDebugInformation>true</GenerateDebugInformation>
+    </Link>
+  </ItemDefinitionGroup>
+  <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
+    <ClCompile>
+      <WarningLevel>Level3</WarningLevel>
+      <SDLCheck>true</SDLCheck>
+      <PreprocessorDefinitions>_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
+      <ConformanceMode>true</ConformanceMode>
+    </ClCompile>
+    <Link>
+      <SubSystem>Console</SubSystem>
+      <GenerateDebugInformation>true</GenerateDebugInformation>
+    </Link>
+  </ItemDefinitionGroup>
+  <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
+    <ClCompile>
+      <WarningLevel>Level3</WarningLevel>
+      <FunctionLevelLinking>true</FunctionLevelLinking>
+      <IntrinsicFunctions>true</IntrinsicFunctions>
+      <SDLCheck>true</SDLCheck>
+      <PreprocessorDefinitions>NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
+      <ConformanceMode>true</ConformanceMode>
+    </ClCompile>
+    <Link>
+      <SubSystem>Console</SubSystem>
+      <EnableCOMDATFolding>true</EnableCOMDATFolding>
+      <OptimizeReferences>true</OptimizeReferences>
+      <GenerateDebugInformation>true</GenerateDebugInformation>
+    </Link>
+  </ItemDefinitionGroup>
+  <ItemGroup>
+    <ClCompile Include="..\xpdev\genwrap.c" />
+    <ClCompile Include="..\xpdev\sockwrap.c" />
+    <ClCompile Include="vdmodem.c" />
+  </ItemGroup>
+  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
+  <ImportGroup Label="ExtensionTargets">
+  </ImportGroup>
+</Project>
\ No newline at end of file