Added Linux proxy server to test seclink.

This commit is contained in:
Scott Duensing 2026-03-10 21:48:08 -05:00
parent bc102b7215
commit b3ae75cf0c
13 changed files with 648 additions and 15 deletions

View file

@ -22,7 +22,7 @@ does not open or close the serial port itself.
Before byte stuffing:
```
[0x7E] [SEQ] [TYPE] [LEN_LO] [LEN_HI] [PAYLOAD...] [CRC_LO] [CRC_HI]
[0x7E] [SEQ] [TYPE] [LEN] [PAYLOAD...] [CRC_LO] [CRC_HI]
```
| Field | Size | Description |
@ -30,8 +30,8 @@ Before byte stuffing:
| `0x7E` | 1 byte | Frame delimiter (flag byte) |
| `SEQ` | 1 byte | Sequence number (wrapping uint8) |
| `TYPE` | 1 byte | Frame type (see below) |
| `LEN` | 2 bytes | Payload length, little-endian |
| Payload | 0-256 | Application data |
| `LEN` | 1 byte | Payload length (0-255) |
| Payload | 0-255 | Application data |
| `CRC` | 2 bytes | CRC-16-CCITT over SEQ+TYPE+LEN+payload |
### Frame Types
@ -82,7 +82,7 @@ typedef struct PktConnS PktConnT;
| Name | Value | Description |
|-----------------------|-------|-------------------------------------|
| `PKT_MAX_PAYLOAD` | 256 | Max payload bytes per packet |
| `PKT_MAX_PAYLOAD` | 255 | Max payload bytes per packet |
| `PKT_DEFAULT_WINDOW` | 4 | Default sliding window size |
| `PKT_MAX_WINDOW` | 8 | Maximum sliding window size |
| `PKT_SUCCESS` | 0 | Success |

View file

@ -1,7 +1,7 @@
// Packetized serial transport with HDLC-style framing and sliding window
//
// Frame format (before byte stuffing):
// [0x7E] [SEQ] [TYPE] [LEN_LO] [LEN_HI] [PAYLOAD...] [CRC_LO] [CRC_HI]
// [0x7E] [SEQ] [TYPE] [LEN] [PAYLOAD...] [CRC_LO] [CRC_HI]
//
// Byte stuffing:
// 0x7E -> 0x7D 0x5E
@ -32,8 +32,8 @@
#define FRAME_NAK 0x02
#define FRAME_RST 0x03
// Header size: SEQ + TYPE + LEN_LO + LEN_HI
#define HEADER_SIZE 4
// Header size: SEQ + TYPE + LEN
#define HEADER_SIZE 3
// CRC size
#define CRC_SIZE 2
// Minimum frame size (header + CRC, no payload)
@ -173,13 +173,12 @@ static void sendFrame(PktConnT *conn, uint8_t seq, uint8_t type, const uint8_t *
uint16_t crc;
int out;
// Build raw frame: SEQ + TYPE + LEN_LO + LEN_HI + PAYLOAD
// Build raw frame: SEQ + TYPE + LEN + PAYLOAD
raw[0] = seq;
raw[1] = type;
raw[2] = (uint8_t)(len & 0xFF);
raw[3] = (uint8_t)((len >> 8) & 0xFF);
raw[2] = (uint8_t)len;
if (payload && len > 0) {
memcpy(&raw[4], payload, len);
memcpy(&raw[3], payload, len);
}
rawLen = HEADER_SIZE + len;
@ -269,7 +268,7 @@ static void processFrame(PktConnT *conn, const uint8_t *frame, int len) {
seq = frame[0];
type = frame[1];
payloadLen = frame[2] | ((int)frame[3] << 8);
payloadLen = frame[2];
// Validate payload length against actual frame size
if (payloadLen + MIN_FRAME_SIZE != len) {

View file

@ -8,7 +8,7 @@
#include <stdbool.h>
// Maximum payload per packet (excluding header/CRC)
#define PKT_MAX_PAYLOAD 256
#define PKT_MAX_PAYLOAD 255
// Default sliding window size (1-8)
#define PKT_DEFAULT_WINDOW 4

49
proxy/Makefile Normal file
View file

@ -0,0 +1,49 @@
# SecLink Proxy — Linux build
# Compiles the packet, security, and secLink layers against a socket
# shim instead of the DJGPP rs232 driver.
CC = gcc
CFLAGS = -O2 -Wall -Wextra
OBJDIR = ../obj/proxy
BINDIR = ../bin
TARGET = $(BINDIR)/secproxy
OBJS = $(OBJDIR)/sockShim.o $(OBJDIR)/packet.o $(OBJDIR)/security.o \
$(OBJDIR)/secLink.o $(OBJDIR)/proxy.o
.PHONY: all clean
all: $(TARGET)
$(TARGET): $(OBJS) | $(BINDIR)
$(CC) -o $@ $(OBJS)
# Local sources
$(OBJDIR)/sockShim.o: sockShim.c sockShim.h | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/proxy.o: proxy.c sockShim.h | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
# Packet layer — block real rs232.h, inject socket shim
$(OBJDIR)/packet.o: ../packet/packet.c sockShim.h | $(OBJDIR)
$(CC) $(CFLAGS) -I. -Istubs/ -include sockShim.h -c -o $@ $<
# Security layer — stub DOS-specific headers
$(OBJDIR)/security.o: ../security/security.c | $(OBJDIR)
$(CC) $(CFLAGS) -Istubs/ -c -o $@ $<
# SecLink layer — block real rs232.h, inject socket shim
$(OBJDIR)/secLink.o: ../seclink/secLink.c sockShim.h | $(OBJDIR)
$(CC) $(CFLAGS) -I. -include sockShim.h -c -o $@ $<
$(OBJDIR):
mkdir -p $(OBJDIR)
$(BINDIR):
mkdir -p $(BINDIR)
clean:
rm -rf $(OBJDIR) $(TARGET)

132
proxy/README.md Normal file
View file

@ -0,0 +1,132 @@
# SecLink Proxy
Linux-hosted proxy that bridges an 86Box emulated serial port to a
remote telnet BBS. The 86Box side communicates using the secLink
protocol (packet framing, DH key exchange, XTEA encryption). The BBS
side is plain telnet over TCP.
## Architecture
```
86Box (DOS terminal) Remote BBS
| |
emulated modem telnet:23
| |
TCP:2323 TCP:23
| |
+--- secproxy ----------------------------------+
secLink ←→ plaintext
(encrypted, reliable)
```
The proxy accepts a single TCP connection from 86Box, performs the
secLink handshake (DH key exchange), then connects to the BBS. All
traffic between 86Box and the proxy is encrypted via XTEA-CTR on
channel 0. Traffic between the proxy and the BBS is unencrypted
telnet.
## Usage
```
secproxy [listen_port] [bbs_host] [bbs_port]
```
| Argument | Default | Description |
|---------------|------------------------|---------------------------------|
| `listen_port` | 2323 | TCP port for 86Box connection |
| `bbs_host` | bbs.duensing.digital | BBS hostname |
| `bbs_port` | 23 | BBS TCP port |
```
secproxy # all defaults
secproxy 5000 # listen on port 5000
secproxy 2323 bbs.example.com 23 # different BBS
secproxy --help # show usage
```
## Startup Sequence
1. Listen on the configured TCP port
2. Wait for 86Box to connect (blocks on accept)
3. Connect to the remote BBS
4. Seed the RNG from `/dev/urandom`
5. Open secLink and perform the DH handshake (blocks until the DOS
side completes its handshake)
6. Enter the proxy loop
## Proxy Loop
The main loop uses `poll()` with a 10ms timeout to multiplex between
the two TCP connections:
- **86Box → BBS**: `secLinkPoll()` reads from the 86Box socket via
the socket shim, decrypts incoming packets, and the receive callback
writes plaintext to the BBS socket.
- **BBS → 86Box**: `read()` from the BBS socket, then
`secLinkSend()` encrypts and sends to 86Box via the socket shim.
- **Maintenance**: `secLinkPoll()` also handles packet-layer retransmit
timers on every iteration.
The proxy exits cleanly on Ctrl+C (SIGINT), SIGTERM, or when either
side disconnects.
## 86Box Configuration
Configure the 86Box serial port to use a telnet connection:
1. In 86Box settings, set a COM port to "TCP (server)" or
"TCP (client)" mode pointing at the proxy's listen port
2. Enable "No telnet negotiation" to send raw bytes
3. The DOS terminal application running inside 86Box uses secLink
over this serial port
## Socket Shim
The proxy reuses the same packet, security, and secLink source code
as the DOS build. A socket shim (`sockShim.h`/`sockShim.c`) provides
rs232-compatible `rs232Read()`/`rs232Write()` functions backed by TCP
sockets instead of UART hardware:
| rs232 function | Socket shim behavior |
|----------------|-----------------------------------------------|
| `rs232Open()` | No-op (socket already connected) |
| `rs232Close()` | Marks port closed (socket managed by caller) |
| `rs232Read()` | Non-blocking `recv()` with `MSG_DONTWAIT` |
| `rs232Write()` | Blocking `send()` loop with `MSG_NOSIGNAL` |
The shim maps COM port indices (0-3) to socket file descriptors via
`sockShimSetFd()`, which must be called before opening the secLink
layer.
DOS-specific headers (`<pc.h>`, `<go32.h>`, `<sys/farptr.h>`) are
replaced by minimal stubs in `stubs/` that provide no-op
implementations. The security library's hardware entropy function
returns zeros on Linux, which is harmless since the proxy seeds the
RNG from `/dev/urandom` before the handshake.
## Building
```
make # builds ../bin/secproxy
make clean # removes objects and binary
```
Objects are placed in `../obj/proxy/`, the binary in `../bin/`.
Requires only a standard Linux C toolchain (gcc, libc). No external
dependencies.
## Files
```
proxy/
proxy.c main proxy program
sockShim.h rs232-compatible socket API (header)
sockShim.c socket shim implementation
Makefile Linux build
stubs/
pc.h stub for DJGPP <pc.h>
go32.h stub for DJGPP <go32.h>
sys/
farptr.h stub for DJGPP <sys/farptr.h>
```

296
proxy/proxy.c Normal file
View file

@ -0,0 +1,296 @@
// SecLink proxy — bridges an 86Box serial connection to a telnet BBS
//
// 86Box (DOS terminal) ←→ TCP ←→ proxy ←→ TCP ←→ BBS
// secLink protocol plain telnet
//
// Usage: proxy [listen_port] [bbs_host] [bbs_port]
// Defaults: 2323 bbs.duensing.digital 23
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <poll.h>
#include <fcntl.h>
#include "sockShim.h"
#include "../seclink/secLink.h"
#include "../security/security.h"
// ========================================================================
// Defines
// ========================================================================
#define DEFAULT_LISTEN_PORT 2323
#define DEFAULT_BBS_HOST "bbs.duensing.digital"
#define DEFAULT_BBS_PORT 23
#define CHANNEL_TERMINAL 0
#define POLL_TIMEOUT_MS 10
// ========================================================================
// Static globals
// ========================================================================
static volatile bool sRunning = true;
// ========================================================================
// Static prototypes (alphabetical)
// ========================================================================
static int connectToBbs(const char *host, int port);
static int createListenSocket(int port);
static void onRecvFromDos(void *ctx, const uint8_t *data, int len, uint8_t channel);
static void seedRng(void);
static void sigHandler(int sig);
// ========================================================================
// Static functions (alphabetical)
// ========================================================================
static int connectToBbs(const char *host, int port) {
struct addrinfo hints;
struct addrinfo *res;
char portStr[16];
int fd;
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
snprintf(portStr, sizeof(portStr), "%d", port);
if (getaddrinfo(host, portStr, &hints, &res) != 0) {
return -1;
}
fd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
if (fd < 0) {
freeaddrinfo(res);
return -1;
}
if (connect(fd, res->ai_addr, res->ai_addrlen) < 0) {
close(fd);
freeaddrinfo(res);
return -1;
}
freeaddrinfo(res);
return fd;
}
static int createListenSocket(int port) {
struct sockaddr_in addr;
int fd;
int opt = 1;
fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) {
return -1;
}
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(port);
if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
close(fd);
return -1;
}
if (listen(fd, 1) < 0) {
close(fd);
return -1;
}
return fd;
}
static void onRecvFromDos(void *ctx, const uint8_t *data, int len, uint8_t channel) {
int bbsFd = *(int *)ctx;
(void)channel;
int sent = 0;
while (sent < len) {
ssize_t n = write(bbsFd, data + sent, len - sent);
if (n <= 0) {
break;
}
sent += (int)n;
}
}
static void seedRng(void) {
uint8_t entropy[32];
FILE *f;
f = fopen("/dev/urandom", "rb");
if (f) {
if (fread(entropy, 1, sizeof(entropy), f) < sizeof(entropy)) {
fprintf(stderr, "Warning: short read from /dev/urandom\n");
}
fclose(f);
}
secRngSeed(entropy, sizeof(entropy));
}
static void sigHandler(int sig) {
(void)sig;
sRunning = false;
}
// ========================================================================
// Main
// ========================================================================
int main(int argc, char *argv[]) {
int listenPort = DEFAULT_LISTEN_PORT;
const char *bbsHost = DEFAULT_BBS_HOST;
int bbsPort = DEFAULT_BBS_PORT;
int listenFd;
int clientFd;
int bbsFd;
SecLinkT *link;
struct sockaddr_in clientAddr;
socklen_t clientLen;
struct pollfd fds[2];
int rc;
if (argc > 1 && (strcmp(argv[1], "-h") == 0 || strcmp(argv[1], "--help") == 0)) {
printf("Usage: %s [listen_port] [bbs_host] [bbs_port]\n", argv[0]);
printf("Defaults: %d %s %d\n", DEFAULT_LISTEN_PORT, DEFAULT_BBS_HOST, DEFAULT_BBS_PORT);
return 0;
}
if (argc > 1) {
listenPort = atoi(argv[1]);
}
if (argc > 2) {
bbsHost = argv[2];
}
if (argc > 3) {
bbsPort = atoi(argv[3]);
}
signal(SIGPIPE, SIG_IGN);
signal(SIGINT, sigHandler);
signal(SIGTERM, sigHandler);
// Listen for 86Box connection
listenFd = createListenSocket(listenPort);
if (listenFd < 0) {
fprintf(stderr, "Failed to listen on port %d: %s\n", listenPort, strerror(errno));
return 1;
}
printf("Listening on port %d...\n", listenPort);
// Accept connection from 86Box
clientLen = sizeof(clientAddr);
clientFd = accept(listenFd, (struct sockaddr *)&clientAddr, &clientLen);
close(listenFd);
if (clientFd < 0) {
fprintf(stderr, "Accept failed: %s\n", strerror(errno));
return 1;
}
printf("86Box connected.\n");
// Associate socket with COM0 for the secLink stack
sockShimSetFd(0, clientFd);
// Connect to BBS
printf("Connecting to %s:%d...\n", bbsHost, bbsPort);
bbsFd = connectToBbs(bbsHost, bbsPort);
if (bbsFd < 0) {
fprintf(stderr, "Failed to connect to BBS: %s\n", strerror(errno));
close(clientFd);
return 1;
}
printf("BBS connected.\n");
// Seed RNG from /dev/urandom and open secLink
seedRng();
link = secLinkOpen(0, 115200, 8, 'N', 1, 0, onRecvFromDos, &bbsFd);
if (!link) {
fprintf(stderr, "Failed to open secLink.\n");
close(bbsFd);
close(clientFd);
return 1;
}
// DH key exchange (blocks until both sides complete)
printf("Waiting for secLink handshake...\n");
rc = secLinkHandshake(link);
if (rc != SECLINK_SUCCESS) {
fprintf(stderr, "Handshake failed: %d\n", rc);
secLinkClose(link);
close(bbsFd);
return 1;
}
printf("Handshake complete. Proxying traffic.\n");
// Set BBS socket non-blocking for the main loop
int flags = fcntl(bbsFd, F_GETFL, 0);
fcntl(bbsFd, F_SETFL, flags | O_NONBLOCK);
// Main proxy loop
fds[0].fd = clientFd;
fds[0].events = POLLIN;
fds[1].fd = bbsFd;
fds[1].events = POLLIN;
while (sRunning) {
poll(fds, 2, POLL_TIMEOUT_MS);
// Process incoming secLink packets from 86Box
// (callback forwards decrypted data to BBS)
secLinkPoll(link);
// Read from BBS and send encrypted to 86Box
if (fds[1].revents & POLLIN) {
uint8_t buf[SECLINK_MAX_PAYLOAD];
ssize_t n = read(bbsFd, buf, sizeof(buf));
if (n <= 0) {
printf("BBS disconnected.\n");
break;
}
secLinkSend(link, buf, (int)n, CHANNEL_TERMINAL, true, true);
}
// Check for disconnects
if (fds[0].revents & (POLLERR | POLLHUP)) {
printf("86Box disconnected.\n");
break;
}
if (fds[1].revents & (POLLERR | POLLHUP)) {
printf("BBS disconnected.\n");
break;
}
}
printf("Shutting down.\n");
secLinkClose(link);
close(bbsFd);
close(clientFd);
return 0;
}

99
proxy/sockShim.c Normal file
View file

@ -0,0 +1,99 @@
// Socket shim — rs232-compatible API over TCP sockets
#include "sockShim.h"
#include <sys/socket.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
// ========================================================================
// Internal state
// ========================================================================
#define MAX_PORTS 4
static int sFds[MAX_PORTS] = { -1, -1, -1, -1 };
static bool sOpen[MAX_PORTS] = { false, false, false, false };
// ========================================================================
// Public functions (alphabetical)
// ========================================================================
int rs232Close(int com) {
if (com < 0 || com >= MAX_PORTS) {
return RS232_ERR_INVALID_PORT;
}
sOpen[com] = false;
// Socket lifecycle is managed by the caller, not the shim
return RS232_SUCCESS;
}
int rs232Open(int com, int32_t bps, int dataBits, char parity, int stopBits, int handshake) {
(void)bps;
(void)dataBits;
(void)parity;
(void)stopBits;
(void)handshake;
if (com < 0 || com >= MAX_PORTS) {
return RS232_ERR_INVALID_PORT;
}
if (sFds[com] < 0) {
return RS232_ERR_NOT_OPEN;
}
sOpen[com] = true;
return RS232_SUCCESS;
}
int rs232Read(int com, char *data, int len) {
if (com < 0 || com >= MAX_PORTS || sFds[com] < 0) {
return 0;
}
ssize_t n = recv(sFds[com], data, len, MSG_DONTWAIT);
if (n <= 0) {
return 0;
}
return (int)n;
}
int rs232Write(int com, const char *data, int len) {
if (com < 0 || com >= MAX_PORTS || sFds[com] < 0) {
return RS232_ERR_NOT_OPEN;
}
int sent = 0;
while (sent < len) {
ssize_t n = send(sFds[com], data + sent, len - sent, MSG_NOSIGNAL);
if (n < 0) {
if (errno == EINTR) {
continue;
}
return RS232_ERR_NOT_OPEN;
}
sent += (int)n;
}
return RS232_SUCCESS;
}
void sockShimSetFd(int com, int fd) {
if (com < 0 || com >= MAX_PORTS) {
return;
}
sFds[com] = fd;
// Set non-blocking so rs232Read returns immediately when empty
int flags = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}

36
proxy/sockShim.h Normal file
View file

@ -0,0 +1,36 @@
// Socket shim — provides rs232-compatible API backed by TCP sockets
// Used by the Linux proxy to reuse the packet and secLink layers.
#ifndef SOCKSHIM_H
#define SOCKSHIM_H
// Block the real rs232.h from being included
#define RS232_H
#include <stdint.h>
#include <stdbool.h>
// rs232-compatible constants (subset used by packet and secLink)
#define RS232_COM1 0
#define RS232_COM2 1
#define RS232_COM3 2
#define RS232_COM4 3
#define RS232_HANDSHAKE_NONE 0
#define RS232_SUCCESS 0
#define RS232_ERR_NOT_OPEN -2
#define RS232_ERR_INVALID_PORT -5
// Associate a socket fd with a COM index. Must be called before
// rs232Open/pktOpen/secLinkOpen. Sets the socket to non-blocking.
void sockShimSetFd(int com, int fd);
// rs232-compatible functions backed by TCP sockets
int rs232Close(int com);
int rs232Open(int com, int32_t bps, int dataBits, char parity, int stopBits, int handshake);
int rs232Read(int com, char *data, int len);
int rs232Write(int com, const char *data, int len);
#endif

7
proxy/stubs/go32.h Normal file
View file

@ -0,0 +1,7 @@
// Stub for DJGPP <go32.h> — Linux proxy build
#ifndef GO32_H_STUB
#define GO32_H_STUB
#define _dos_ds 0
#endif

8
proxy/stubs/pc.h Normal file
View file

@ -0,0 +1,8 @@
// Stub for DJGPP <pc.h> — Linux proxy build
#ifndef PC_H_STUB
#define PC_H_STUB
static inline void outportb(unsigned short port, unsigned char val) { (void)port; (void)val; }
static inline unsigned char inportb(unsigned short port) { (void)port; return 0; }
#endif

7
proxy/stubs/sys/farptr.h Normal file
View file

@ -0,0 +1,7 @@
// Stub for DJGPP <sys/farptr.h> — Linux proxy build
#ifndef FARPTR_H_STUB
#define FARPTR_H_STUB
static inline unsigned long _farpeekl(unsigned short sel, unsigned long ofs) { (void)sel; (void)ofs; return 0; }
#endif

View file

@ -62,7 +62,7 @@ typedef struct SecLinkS SecLinkT;
| Name | Value | Description |
|-------------------------|-------|----------------------------------------|
| `SECLINK_MAX_PAYLOAD` | 255 | Max bytes per `secLinkSend()` call |
| `SECLINK_MAX_PAYLOAD` | 254 | Max bytes per `secLinkSend()` call |
| `SECLINK_MAX_CHANNEL` | 127 | Highest valid channel number |
| `SECLINK_SUCCESS` | 0 | Operation succeeded |
| `SECLINK_ERR_PARAM` | -1 | Invalid parameter |

View file

@ -27,7 +27,7 @@
#define SECLINK_ERR_SEND -6
// Max plaintext payload per send (packet max minus 1-byte channel header)
#define SECLINK_MAX_PAYLOAD 255
#define SECLINK_MAX_PAYLOAD 254
// Channel limits
#define SECLINK_MAX_CHANNEL 127