diff --git a/src/sbbs3/GNUmakefile b/src/sbbs3/GNUmakefile
index f92fb53ff77edd70dba16000e6e5e4d06abfd73c..8bbf4daa1110d969ed27a179025fd6eaf3c95d5a 100644
--- a/src/sbbs3/GNUmakefile
+++ b/src/sbbs3/GNUmakefile
@@ -147,7 +147,7 @@ $(BAJA): $(BAJA_OBJS)
 	$(QUIET)$(CC) $(UTIL_LDFLAGS) -o $@ $(BAJA_OBJS) $(SMBLIB_LIBS) $(XPDEV_LIBS)
 
 # UnBaja Utility
-$(UNBAJA): $(UNBAJA_OBJS)
+$(UNBAJA): $(OBJODIR) $(EXEODIR) $(UNBAJA_OBJS)
 	@echo Linking $@
 	$(QUIET)$(CC) $(UTIL_LDFLAGS) -o $@ $(UNBAJA_OBJS) $(XPDEV_LIBS)
 
@@ -215,3 +215,9 @@ $(ASC2ANS): $(OBJODIR)/asc2ans.o
 $(SEXYZ): $(SEXYZ_OBJS)
 	@echo Linking $@
 	$(QUIET)$(CC) $(LDFLAGS) $(MT_LDFLAGS) -o $@ $(SEXYZ_OBJS) $(SMBLIB_LIBS) $(XPDEV-MT_LIBS)
+
+# QWKNODES
+$(QWKNODES): $(QWKNODES_OBJS)
+	@echo Linking $@
+	$(QUIET)$(CC) $(UTIL_LDFLAGS) -o $@ $(QWKNODES_OBJS) $(SMBLIB_LIBS) $(XPDEV_LIBS)
+
diff --git a/src/sbbs3/Makefile b/src/sbbs3/Makefile
index 52efd889b9a1dea335e5e9477b399f88e1bab10b..b5d416c525d36749d55eff5bbc4c61578394bef1 100644
--- a/src/sbbs3/Makefile
+++ b/src/sbbs3/Makefile
@@ -201,3 +201,8 @@ $(MTOBJODIR)$(DIRSEP)services$(OFILE): services.c
         $(QUIET)$(CC) $(CFLAGS) $(CCFLAGS) -DSERVICES_EXPORTS -DSMB_IMPORTS -USBBS_EXPORTS \
 			-n$(MTOBJODIR) $(MT_CFLAGS) -c $** $(OUTPUT)$@
 
+# QWKNodes
+$(QWKNODES): $(QWKNODES_OBJS)
+	@echo Linking $@
+	$(QUIET)$(CC) $(UTIL_LDFLAGS) -e$@ $** $(SMBLIB_LIBS) $(XPDEV_LIBS)
+
diff --git a/src/sbbs3/objects.mk b/src/sbbs3/objects.mk
index bd68448d8a44fc4fe5eb50b2269fa32cc36cc0e5..5f3fec2a0bc1770678ca0933ec37fd419bf634cd 100644
--- a/src/sbbs3/objects.mk
+++ b/src/sbbs3/objects.mk
@@ -230,3 +230,13 @@ SEXYZ_OBJS = \
 			$(MTOBJODIR)$(DIRSEP)ringbuf$(OFILE) \
 			$(MTOBJODIR)$(DIRSEP)nopen$(OFILE) \
 			$(MTOBJODIR)$(DIRSEP)telnet$(OFILE)
+
+QWKNODES_OBJS = \
+			$(OBJODIR)$(DIRSEP)qwknodes$(OFILE)\
+			$(OBJODIR)$(DIRSEP)date_str$(OFILE)\
+			$(OBJODIR)$(DIRSEP)nopen$(OFILE) \
+			$(OBJODIR)$(DIRSEP)load_cfg$(OFILE)\
+			$(OBJODIR)$(DIRSEP)scfglib1$(OFILE) \
+			$(OBJODIR)$(DIRSEP)scfglib2$(OFILE) \
+			$(OBJODIR)$(DIRSEP)str_util$(OFILE) \
+			$(OBJODIR)$(DIRSEP)ars$(OFILE) \
diff --git a/src/sbbs3/qwknodes.c b/src/sbbs3/qwknodes.c
index 351f8111de07881774d0865ec7d78f9a8c7f4e27..139a5bff4a65d58cb58504f40d11aa26f70c0aac 100644
--- a/src/sbbs3/qwknodes.c
+++ b/src/sbbs3/qwknodes.c
@@ -35,157 +35,18 @@
  * Note: If this box doesn't appear square, then you need to fix your tabs.	*
  ****************************************************************************/
 
+#include "gen_defs.h"
+#include "genwrap.h"
+#include "dirwrap.h"
 #include "sbbs.h"
+#include "nopen.h"
+#include "crc16.h"
 #include "crc32.h"
 
 unsigned _stklen=10000;
 smb_t		smb;
 scfg_t		cfg;
 
-/****************************************************************************/
-/* Updates 16-bit "rcrc" with character 'ch'                                */
-/****************************************************************************/
-void ucrc16(uchar ch, ushort *rcrc) {
-	ushort i, cy;
-    uchar nch=ch;
- 
-for (i=0; i<8; i++) {
-    cy=*rcrc & 0x8000;
-    *rcrc<<=1;
-    if (nch & 0x80) *rcrc |= 1;
-    nch<<=1;
-    if (cy) *rcrc ^= 0x1021; }
-}
-
-/****************************************************************************/
-/* Returns 16-crc of string (not counting terminating NULL) 				*/
-/****************************************************************************/
-ushort crc16(char *str)
-{
-	int 	i=0;
-	ushort	crc=0;
-
-ucrc16(0,&crc);
-while(str[i])
-	ucrc16(str[i++],&crc);
-ucrc16(0,&crc);
-ucrc16(0,&crc);
-return(crc);
-}
-
-/****************************************************************************/
-/* Returns 32-crc of string (not counting terminating NULL) 				*/
-/****************************************************************************/
-ulong _crc32(char *str)
-{
-	int i=0;
-	ulong crc=0xffffffffUL;
-
-	while(str[i])
-		crc=ucrc32(str[i++],crc);
-	crc=~crc;
-	return(crc);
-}
-
-/****************************************************************************/
-/* Converts unix time format (long - time_t) into a char str MM/DD/YY		*/
-/****************************************************************************/
-char * unixtodstr(time_t unix, char *str)
-{
-	struct tm* tm;
-
-if(!unix)
-	strcpy(str,"00/00/00");
-else {
-	tm=localtime(&unix);
-	if(tm==NULL) {
-		strcpy(str,"00/00/00");
-		return(str);
-	}
-	if(tm->tm_mon>11) {	  /* DOS leap year bug */
-		tm->tm_mon=0;
-		tm->tm_year++; }
-	if(tm->tm_mday>31)
-		tm->tm_mday=1;
-	if(cfg.sys_misc&SM_EURODATE)
-		sprintf(str,"%02u/%02u/%02u",tm->tm_mday,tm->tm_mon+1
-			,TM_YEAR(tm->tm_year));
-	else
-		sprintf(str,"%02u/%02u/%02u",tm->tm_mon+1,tm->tm_mday
-			,TM_YEAR(tm->tm_year)); }
-return(str);
-}
-
-/****************************************************************************/
-/* Puts a backslash on path strings 										*/
-/****************************************************************************/
-void backslash(char *str)
-{
-    int i;
-
-i=strlen(str);
-if(i && str[i-1]!='\\') {
-    str[i]='\\'; str[i+1]=0; }
-}
-/****************************************************************************/
-/* Network open function. Opens all files DENYALL and retries LOOP_NOPEN    */
-/* number of times if the attempted file is already open or denying access	*/
-/* for some other reason.	All files are opened in BINARY mode.			*/
-/****************************************************************************/
-int nopen(char *str, int access)
-{
-	int file,share,count=0;
-
-if(access==O_RDONLY) share=SH_DENYWR;
-	else share=SH_DENYRW;
-while(((file=sopen(str,O_BINARY|access,share,S_IWRITE))==-1)
-	&& (errno==EACCES errno==EAGAIN) && count++<LOOP_NOPEN);
-if(file==-1 && (errno==EACCES || errno==EAGAIN))
-	lputs("\7\r\nNOPEN: ACCESS DENIED\r\n\7");
-return(file);
-}
-/****************************************************************************/
-/* This function performs an nopen, but returns a file stream with a buffer */
-/* allocated.																*/
-/****************************************************************************/
-FILE *fnopen(int *file, char *str, int access)
-{
-	char mode[128];
-	FILE *stream;
-
-if(((*file)=nopen(str,access))==-1)
-	return(NULL);
-
-if(access&O_APPEND) {
-	if(access&O_RDONLY)
-		strcpy(mode,"a+");
-	else
-		strcpy(mode,"a"); }
-else {
-	if(access&O_WRONLY)
-		strcpy(mode,"r+");
-	else
-		strcpy(mode,"r"); }
-stream=fdopen((*file),mode);
-if(stream==NULL) {
-	close(*file);
-	return(NULL); }
-setvbuf(stream,NULL,_IOFBF,16*1024);
-return(stream);
-}
-
-/****************************************************************************/
-/* Truncates white-space chars off end of 'str' and terminates at first tab */
-/****************************************************************************/
-void truncsp(char *str)
-{
-	uchar c;
-
-c=strlen(str);
-while(c && (uchar)str[c-1]<=SP) c--;
-str[c]=0;
-}
-
 void stripctrla(uchar *str)
 {
 	uchar out[256];
@@ -237,7 +98,6 @@ void bail(int code)
 	exit(code);
 }
 
-
 char *loadmsgtail(smbmsg_t msg)
 {
 	char	*buf=NULL;
@@ -279,7 +139,7 @@ if(!strnicmp(p," 
 	p+=16;
 if(!strnicmp(p," * Synchronet * ",16))
     p+=16;
-while(*p && *p<=SP) p++;
+while(*p && *p<=' ') p++;
 strcpy(tag,p);
 FREE(buf);
 }
@@ -298,16 +158,16 @@ char *usage="\nusage: qwknodes [/opts] cmds"
 			"\n"
 			"\n cmds: r  =  create route.dat"
 			"\n       u  =  create users.dat"
-			"\n       n  =  create nodes.dat
+			"\n       n  =  create nodes.dat"
 			"\n"
 			"\n opts: f  =  format addresses for nodes that feed from this system"
 			"\n       a  =  append existing output files"
-			"\n       t  =  include tag lines in nodes.dat
-			"\n       l  =  include local users in users.dat
+			"\n       t  =  include tag lines in nodes.dat"
+			"\n       l  =  include local users in users.dat"
 			"\n       m# =  maximum message age set to # days"
 			"\n";
 
-void main(int argc, char **argv)
+int main(int argc, char **argv)
 {
 	char			str[256],tmp[128],tag[256],addr[256],*p;
 	int 			i,j,mode=0,cmd=0,o_mode,max_age=0;
@@ -315,14 +175,8 @@ void main(int argc, char **argv)
 	ulong			*crc=NULL,curcrc,total_crcs=0,l;
 	FILE			*route,*users,*nodes;
 	time_t			now;
-	read_cfg_text_t txt;
 	smbmsg_t		msg;
-
-txt.openerr="\7\r\nError opening %s for read.\r\n";
-txt.reading="\r\nReading %s...";
-txt.readit="\rRead %s       ";
-txt.allocerr="\7\r\nError allocating %u bytes of memory\r\n";
-txt.error="\7\r\nERROR: Offset %lu in %s\r\n\r\n";
+	char			*ctrl_dir;
 
 fprintf(stderr,"\nSynchronet QWKnet Node/Route/User List  v1.20  "
 	"Copyright 2000 Rob Swindell\n");
@@ -354,7 +208,7 @@ for(i=1;i<argc;i++)
 							break;
 						default:
 							printf(usage);
-							exit(1); }
+							return(1); }
 				j--;
 				break;
 			case 'R':
@@ -368,11 +222,11 @@ for(i=1;i<argc;i++)
 				break;
 			default:
 				printf(usage);
-				exit(1); }
+				return(1); }
 
 if(!cmd) {
 	printf(usage);
-	exit(1); }
+	return(1); }
 
 if(mode&APPEND)
 	o_mode=O_WRONLY|O_CREAT|O_APPEND;
@@ -382,32 +236,46 @@ else
 if(cmd&NODES)
 	if((nodes=fnopen(&i,"nodes.dat",o_mode))==NULL) {
 		printf("\7\nError opening nodes.dat\n");
-		exit(1); }
+		return(1); }
 
 if(cmd&USERS)
 	if((users=fnopen(&i,"users.dat",o_mode))==NULL) {
 		printf("\7\nError opening users.dat\n");
-        exit(1); }
+        return(1); }
 
 if(cmd&ROUTE)
 	if((route=fnopen(&i,"route.dat",o_mode))==NULL) {
 		printf("\7\nError opening route.dat\n");
-        exit(1); }
+        return(1); }
+
+cfg.size=sizeof(cfg);
+ctrl_dir=getenv("SBBSCTRL");
+if(ctrl_dir==NULL || ctrl_dir[0]==0) {
+	ctrl_dir="/sbbs/ctrl";          /* Not set? Use default */
+	printf("!SBBSCTRL environment variable not set, using default value: %s\n\n"
+			,ctrl_dir);
+}
+SAFECOPY(cfg.ctrl_dir, ctrl_dir);
 
-if(!node_dir[0]) {
+if(!load_cfg(&cfg, NULL, TRUE, str)) {
+	printf("\7\n%s\n",str);
+}
+
+#if 0
+if(!cfg.node_dir[0]) {
 	p=getenv("SBBSNODE");
 	if(p==NULL) {
 		printf("\7\nSBBSNODE environment variable not set.\n");
-		exit(1); }
-	strcpy(node_dir,p); }
+		return(1); }
+	strcpy(cfg.node_dir,p); }
 
-if(node_dir[strlen(node_dir)-1]!='\\')
-	strcat(node_dir,"\\");
+if(cfg.node_dir[strlen(cfg.node_dir)-1]!='\\')
+	strcat(cfg.node_dir,"\\");
 
 read_node_cfg(&cfg,txt);
 if(ctrl_dir[0]=='.') {   /* Relative path */
 	strcpy(str,ctrl_dir);
-	sprintf(ctrl_dir,"%s%s",node_dir,str);
+	sprintf(ctrl_dir,"%s%s",cfg.node_dir,str);
 	if(FULLPATH(str,ctrl_dir,40))
 		strcpy(ctrl_dir,str); }
 backslash(ctrl_dir);
@@ -415,24 +283,25 @@ backslash(ctrl_dir);
 read_main_cfg(&cfg,txt);
 if(data_dir[0]=='.') {   /* Relative path */
 	strcpy(str,data_dir);
-	sprintf(data_dir,"%s%s",node_dir,str);
+	sprintf(data_dir,"%s%s",cfg.node_dir,str);
 	if(FULLPATH(str,data_dir,40))
 		strcpy(data_dir,str); }
 backslash(data_dir);
 read_msgs_cfg(txt);
+#endif
 
 now=time(NULL);
-smm=crc16("smm");
-sbl=crc16("sbl");
+smm=crc16("smm",0);
+sbl=crc16("sbl",0);
 fprintf(stderr,"\n\n");
-for(i=0;i<total_subs;i++) {
-	if(!(sub[i]->misc&SUB_QNET))
+for(i=0;i<cfg.total_subs;i++) {
+	if(!(cfg.sub[i]->misc&SUB_QNET))
 		continue;
 	fprintf(stderr,"%-*s  %s\n"
-		,LEN_GSNAME,grp[sub[i]->grp]->sname,sub[i]->lname);
-	sprintf(smb.file,"%s%s",sub[i]->data_dir,sub[i]->code);
+		,LEN_GSNAME,cfg.grp[cfg.sub[i]->grp]->sname,cfg.sub[i]->lname);
+	sprintf(smb.file,"%s%s",cfg.sub[i]->data_dir,cfg.sub[i]->code);
 	smb.retry_time=30;
-	smb.sunum=i;
+	smb.subnum=i;
 	if((j=smb_open(&smb))!=0) {
 		printf("smb_open returned %d\n",j);
 		continue; }
@@ -473,9 +342,9 @@ for(i=0;i<total_subs;i++) {
 				msg.from_net.addr="";
 			if(cmd&USERS) {
 				sprintf(str,"%s%s",msg.from_net.addr,msg.from);
-				curcrc=crc32(str); }
+				curcrc=crc32(str,0); }
 			else
-				curcrc=crc32(msg.from_net.addr);
+				curcrc=crc32(msg.from_net.addr,0);
 			for(l=0;l<total_crcs;l++)
 				if(curcrc==crc[l])
 					break;
@@ -496,8 +365,8 @@ for(i=0;i<total_subs;i++) {
 						else
 							*(p++)=0;
 						sprintf(str,"%s %s:%s%c%s"
-							,unixtodstr(msg.hdr.when_written.time,tmp)
-							,p,sys_id,p==addr ? 0 : '/'
+							,unixtodstr(&cfg,msg.hdr.when_written.time,tmp)
+							,p,cfg.sys_id,p==addr ? 0 : '/'
 							,addr);
 						fprintf(route,"%s\r\n",str); }
 					else {
@@ -505,32 +374,32 @@ for(i=0;i<total_subs;i++) {
 						if(p) {
 							*(p++)=0;
 						fprintf(route,"%s %s:%.*s\r\n"
-							,unixtodstr(msg.hdr.when_written.time,str)
+							,unixtodstr(&cfg,msg.hdr.when_written.time,str)
 							,p
 							,(uint)(p-addr)
 							,addr); } } }
 				if(cmd&USERS) {
 					if(msg.from_net.type!=NET_QWK)
-						strcpy(str,sys_id);
+						strcpy(str,cfg.sys_id);
 					else if(mode&FEED)
-						sprintf(str,"%s/%s",sys_id,msg.from_net.addr);
+						sprintf(str,"%s/%s",cfg.sys_id,msg.from_net.addr);
 					else
 						strcpy(str,msg.from_net.addr);
 					p=strrchr(str,'/');
                     if(p)
 						fprintf(users,"%-25.25s  %-8.8s  %s  (%s)\r\n"
 							,msg.from,p+1
-							,unixtodstr(msg.hdr.when_written.time,tmp)
+							,unixtodstr(&cfg,msg.hdr.when_written.time,tmp)
 							,str);
 					else
 						fprintf(users,"%-25.25s  %-8.8s  %s\r\n"
 							,msg.from,str
-							,unixtodstr(msg.hdr.when_written.time,tmp)); }
+							,unixtodstr(&cfg,msg.hdr.when_written.time,tmp)); }
 				if(cmd&NODES && msg.from_net.type==NET_QWK) {
 					if(mode&TAGS)
 						gettag(msg,tag);
 					if(mode&FEED)
-						sprintf(str,"%s/%s",sys_id,msg.from_net.addr);
+						sprintf(str,"%s/%s",cfg.sys_id,msg.from_net.addr);
 					else
 						strcpy(str,msg.from_net.addr);
 					p=strrchr(str,'/');
@@ -542,14 +411,14 @@ for(i=0;i<total_subs;i++) {
 						else
 							fprintf(nodes,"%-8.8s  %s  (%s)\r\n"
 								,p+1
-								,unixtodstr(msg.hdr.when_written.time,tmp)
+								,unixtodstr(&cfg,msg.hdr.when_written.time,tmp)
 								,str); }
 					else
 						fprintf(nodes,"%-8.8s  %s\r\n"
 							,str
 							,mode&TAGS
 							? tag
-							: unixtodstr(msg.hdr.when_written.time,tmp)); }
+							: unixtodstr(&cfg,msg.hdr.when_written.time,tmp)); }
 				} }
 		smb_freemsgmem(&msg); }
 
@@ -560,4 +429,3 @@ for(i=0;i<total_subs;i++) {
 		break; } }
 fprintf(stderr,"Done.\n");
 }
-
diff --git a/src/sbbs3/targets.mk b/src/sbbs3/targets.mk
index d772d8b998c93fc65f0676283935ea3ba54a1f50..12d274b88eaec7a792dabd1452f8b83639750f0b 100644
--- a/src/sbbs3/targets.mk
+++ b/src/sbbs3/targets.mk
@@ -28,12 +28,14 @@ MAKEUSER	= $(EXEODIR)$(DIRSEP)makeuser$(EXEFILE)
 ANS2ASC		= $(EXEODIR)$(DIRSEP)ans2asc$(EXEFILE)
 ASC2ANS		= $(EXEODIR)$(DIRSEP)asc2ans$(EXEFILE)
 SEXYZ		= $(EXEODIR)$(DIRSEP)sexyz$(EXEFILE)
+QWKNODES	= $(EXEODIR)$(DIRSEP)qwknodes$(EXEFILE)
 
 UTILS		= $(BUILD_DEPENDS)$(FIXSMB) $(BUILD_DEPENDS)$(CHKSMB) \
 			  $(BUILD_DEPENDS)$(SMBUTIL) $(BUILD_DEPENDS)$(BAJA) $(BUILD_DEPENDS)$(NODE) \
 			  $(BUILD_DEPENDS)$(SBBSECHO) $(BUILD_DEPENDS)$(ECHOCFG) $(BUILD_DEPENDS) \
 			  $(BUILD_DEPENDS)$(ADDFILES) $(BUILD_DEPENDS)$(FILELIST) $(BUILD_DEPENDS)$(MAKEUSER) \
-			  $(BUILD_DEPENDS)$(ANS2ASC) $(BUILD_DEPENDS)$(ASC2ANS)  $(BUILD_DEPENDS)$(UNBAJA)
+			  $(BUILD_DEPENDS)$(ANS2ASC) $(BUILD_DEPENDS)$(ASC2ANS)  $(BUILD_DEPENDS)$(UNBAJA) \
+			  $(BUILD_DEPENDS)$(QWKNODES)
 
 all:	dlls utils console mono
 
@@ -78,3 +80,4 @@ $(MAKEUSER): $(XPDEV_LIB)
 $(ANS2ASC):
 $(ASC2ANS):
 $(SEXYZ): $(XPDEV-MT_LIB) $(SMBLIB)
+$(QWKNODES): $(XPDEV_LIB)