OXYGENHUB
◆ main openmsx / src / network / EmulatedEsp32.cc
77.2 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
e8555c89 Andy McCall 1
#include "EmulatedEsp32.hh"
2
3
#include "endian.hh"
4
#include "MSXCliComm.hh"
5
#include "narrow.hh"
6
#include "one_of.hh"
7
#include "OpenSSL.hh"
8
#include "stl.hh"
9
#include "StringOp.hh"
10
#include "zstring_view.hh"
11
12
#include <algorithm>
13
#include <array>
14
#include <bit>
15
#include <chrono>
16
#include <cstring>
17
#include <cwctype>
18
#include <fstream>
19
#include <memory>
20
#include <optional>
21
#include <random>
22
#include <span>
23
#include <sstream>
24
#include <string_view>
25
26
#ifndef _WIN32
27
#include <arpa/inet.h>
28
#include <ifaddrs.h>
29
#include <net/if.h>
30
#include <netdb.h>
31
#include <netinet/tcp.h>
32
#include <sys/select.h>
33
#include <sys/socket.h>
34
#include <unistd.h>
35
#else
36
#include <iphlpapi.h>
37
#endif
38
39
namespace openmsx {
40
41
// TCP-IP UNAPI command codes (spec 1.1, section 4; TCPIP_DNS_Q_NEW is an
42
// implementation-specific routine of the ESP32-UNAPI firmware).
43
static constexpr uint8_t UNAPI_GET_INFO     = 0;
44
static constexpr uint8_t TCPIP_GET_CAPAB    = 1;
45
static constexpr uint8_t TCPIP_GET_IPINFO   = 2;
46
static constexpr uint8_t TCPIP_NET_STATE    = 3;
47
static constexpr uint8_t TCPIP_DNS_Q        = 6;
48
static constexpr uint8_t TCPIP_UDP_OPEN     = 8;
49
static constexpr uint8_t TCPIP_UDP_CLOSE    = 9;
50
static constexpr uint8_t TCPIP_UDP_STATE    = 10;
51
static constexpr uint8_t TCPIP_UDP_SEND     = 11;
52
static constexpr uint8_t TCPIP_UDP_RCV      = 12;
53
static constexpr uint8_t TCPIP_TCP_OPEN     = 13;
54
static constexpr uint8_t TCPIP_TCP_CLOSE    = 14;
55
static constexpr uint8_t TCPIP_TCP_ABORT    = 15;
56
static constexpr uint8_t TCPIP_TCP_STATE    = 16;
57
static constexpr uint8_t TCPIP_TCP_SEND     = 17;
58
static constexpr uint8_t TCPIP_TCP_RCV      = 18;
59
static constexpr uint8_t TCPIP_CONFIG_AUTOIP = 25;
60
static constexpr uint8_t TCPIP_CONFIG_IP     = 26;
61
static constexpr uint8_t TCPIP_DNS_Q_NEW     = 206;
62
63
// ---- helper: check if a custom command is "quick" (no parameters) ----
64
// Mirrors the QUICK_COMMAND set executed directly in the RX_PARSER_IDLE
65
// state of the ESP32 UNAPI firmware (see received_data_parser()). The
66
// full custom command set is documented in the ESP32-UNAPI firmware
67
// documentation:
68
// https://github.com/ducasp/ESP32-UNAPI-Firmware/tree/main/documentation#readme
69
[[nodiscard]] static bool isQuickCustomCommand(uint8_t cmd)
70
{
71
	return cmd == one_of('R', 'W', '?', 'V', 'r',
72
	                     'S', 's', 'g', 'N', 'D',
73
	                     'O', 'o', 'Q', 'c', 'G',
74
	                     'H', 'h', 'a', 'b', 'w', 'E', 'I');
75
}
76
77
// ---- helper: check if a command carries a 2-byte size header + data ----
78
// These commands move the parser to RX_PARSER_WAIT_DATA_SIZE. Mirrors the
79
// command list of the ESP32 firmware's received_data_parser(); any other
80
// byte is discarded while the parser stays IDLE.
81
[[nodiscard]] static bool isDataCommand(uint8_t cmd)
82
{
83
	return cmd == one_of(TCPIP_GET_CAPAB, TCPIP_GET_IPINFO, TCPIP_NET_STATE,
84
	                     TCPIP_DNS_Q,
85
	                     TCPIP_UDP_OPEN, TCPIP_UDP_CLOSE, TCPIP_UDP_STATE,
86
	                     TCPIP_UDP_SEND, TCPIP_UDP_RCV,
87
	                     TCPIP_TCP_OPEN, TCPIP_TCP_CLOSE, TCPIP_TCP_ABORT,
88
	                     TCPIP_TCP_STATE, TCPIP_TCP_SEND, TCPIP_TCP_RCV,
89
	                     TCPIP_CONFIG_AUTOIP, TCPIP_CONFIG_IP,
90
	                     TCPIP_DNS_Q_NEW) ||
91
	       (129 <= cmd && cmd <= 143) || // SSH UNAPI
92
	       (200 <= cmd && cmd <= 202) || // HTTP client
93
	       cmd == one_of('A', 'B', 'd', 'U', 'u',
94
	                     'Z', 'Y', 'z', 'T', 'C');
95
}
96
97
// Max command data block size (MAX_CMD_DATA_LEN in UNAPIESP.h)
98
static constexpr unsigned MAX_CMD_DATA_LEN = 2148;
99
100
// Boot greeting (the device identifies itself as a generic openMSX
101
// TCP/IP UNAPI adapter)
102
static constexpr std::string_view bootGreeting =
103
	"TCP-IP UNAPI openMSX Generic v1.0\r\n";
104
105
// ---- helper: host network configuration ----
106
// Detects the host's primary IPv4 network configuration. Used as a
107
// fallback for GET_IPINFO when the corresponding setting is 0.0.0.0
108
// ("use host value"), like the ESP32 reporting its WiFi IP.
109
struct HostNetInfo
110
{
111
	uint32_t ip = 0;
112
	uint32_t netmask = 0;
113
	uint32_t gateway = 0;
114
	uint32_t dns1 = 0;
115
	uint32_t dns2 = 0;
116
};
117
118
// Parse a dotted-quad IPv4 string into a value in network byte order
119
// (0 on failure)
120
static bool parseIPv4String(std::string_view str, uint32_t& ipOut)
121
{
122
	std::array<unsigned, 4> octets;
123
	int n = 0;
124
	std::string_view rest = str;
125
	while (n < 4) {
126
		auto dot = rest.find('.');
127
		auto v = StringOp::stringToBase<10, unsigned>(rest.substr(0, dot));
128
		if (!v || *v > 255) return false;
129
		octets[n++] = *v;
130
		if (dot == std::string_view::npos) break;
131
		rest.remove_prefix(dot + 1);
132
	}
133
	if (n != 4) return false;
134
	ipOut = (octets[0] << 24) | (octets[1] << 16) | (octets[2] << 8) | octets[3];
135
	return true;
136
}
137
138
#ifdef _WIN32
139
// Adapter name markers of virtual network adapters (virtual switches,
140
// VM/VPN software, ...), matched case-insensitively against the adapter's
141
// friendly name (e.g. "vEthernet (WSL (Hyper-V firewall))").
142
static bool nameContainsVirtual(const WCHAR* name)
143
{
144
	static constexpr auto markers = std::to_array<std::wstring_view>({
145
		L"wsl", L"vethernet", L"hyper-v", L"virtual", L"vmware",
146
		L"virtualbox", L"vbox", L"tailscale", L"zerotier",
147
		L"docker", L"tap-", L"isatap", L"teredo"
148
	});
149
	std::wstring lower;
150
	for (auto c : std::wstring(name)) {
151
		lower.push_back(std::towlower(c));
152
	}
153
	return std::ranges::any_of(markers, [&](const auto& m) {
154
		return lower.contains(m);
155
	});
156
}
157
158
// Preference score for a network adapter: wired > wireless > other
159
// physical > virtual (tunnels, Hyper-V/WSL switch NICs, ...). Negative
160
// scores are never used; virtual adapters are kept as a fallback so
161
// machines that only have one (e.g. WSL-only) still work.
162
static int adapterScore(const IP_ADAPTER_ADDRESSES& a)
163
{
164
	if (a.OperStatus != IfOperStatusUp) return -1;
165
	if (a.IfType == IF_TYPE_SOFTWARE_LOOPBACK) return -1;
166
	if (a.IfType == IF_TYPE_TUNNEL || a.TunnelType != TUNNEL_TYPE_NONE) {
167
		return 0; // tunnel / VPN
168
	}
169
	if (nameContainsVirtual(a.FriendlyName)) return 0;
170
	if (a.IfType == IF_TYPE_ETHERNET_CSMACD) return 3; // wired
171
	if (a.IfType == IF_TYPE_IEEE80211) return 2;       // wireless
172
	return 1; // other physical (PPP, ...)
173
}
174
175
// Fills the host info from the first usable IPv4 address of the given
176
// adapter (loopback excluded); nullopt when it has none.
177
static std::optional<HostNetInfo> hostNetInfoFromAdapter(const IP_ADAPTER_ADDRESSES& a)
178
{
179
	HostNetInfo info;
180
	for (auto* u = a.FirstUnicastAddress; u; u = u->Next) {
181
		auto* sa = std::bit_cast<const sockaddr_in*>(u->Address.lpSockaddr);
182
		if (sa->sin_family != AF_INET) continue;
183
		uint32_t ip = ntohl(sa->sin_addr.s_addr);
184
		if (ip == 0 || (ip & 0xFF000000) == 0x7F000000) continue; // skip loopback
185
		info.ip = ip;
186
		uint8_t prefix = u->OnLinkPrefixLength;
187
		info.netmask = (prefix == 0) ? 0 : (0xFFFFFFFFu << (32 - prefix));
188
		if (a.FirstGatewayAddress) {
189
			auto* g = std::bit_cast<const sockaddr_in*>(a.FirstGatewayAddress->Address.lpSockaddr);
190
			info.gateway = ntohl(g->sin_addr.s_addr);
191
		}
192
		int n = 0;
193
		for (auto* d = a.FirstDnsServerAddress; d && n < 2; d = d->Next) {
194
			auto* dsa = std::bit_cast<const sockaddr_in*>(d->Address.lpSockaddr);
195
			uint32_t dns = ntohl(dsa->sin_addr.s_addr);
196
			auto* dnsSlot = (n == 0) ? &info.dns1 : &info.dns2;
197
			*dnsSlot = dns;
198
			++n;
199
		}
200
		return info;
201
	}
202
	return std::nullopt;
203
}
204
205
static std::optional<HostNetInfo> getHostNetInfo()
206
{
207
	ULONG size = 0;
208
	if (GetAdaptersAddresses(AF_INET, GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST,
209
	                         nullptr, nullptr, &size) != ERROR_BUFFER_OVERFLOW) {
210
		return std::nullopt;
211
	}
212
	if (size == 0) return std::nullopt;
213
	auto buffer = std::make_unique_for_overwrite<uint8_t[]>(size);
214
	auto* adapters = std::bit_cast<PIP_ADAPTER_ADDRESSES>(buffer.get());
215
	if (GetAdaptersAddresses(AF_INET, GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST,
216
	                         nullptr, adapters, &size) != NO_ERROR) {
217
		return std::nullopt;
218
	}
219
220
	// Pick the preferred adapter: the highest-scoring one with a usable
221
	// IPv4 address (ties keep the first one).
222
	int bestScore = -1;
223
	std::optional<HostNetInfo> bestInfo;
224
	for (auto* a = adapters; a; a = a->Next) {
225
		int score = adapterScore(*a);
226
		if (score <= bestScore) continue; // ties keep the first one
227
		auto info = hostNetInfoFromAdapter(*a);
228
		if (!info) continue;
229
		bestScore = score;
230
		bestInfo = std::move(info);
231
	}
232
	return bestInfo;
233
}
234
#else
235
// Read the first two DNS servers from /etc/resolv.conf
236
static void readResolvConfDns(uint32_t& dns1, uint32_t& dns2)
237
{
238
	std::ifstream f("/etc/resolv.conf");
239
	std::string line;
240
	int n = 0;
241
	while (n < 2 && std::getline(f, line)) {
242
		std::string_view entry(line);
243
		StringOp::trim(entry, " \t");
244
		if (entry.empty()) continue;
245
		if (!entry.starts_with("nameserver ")) continue;
246
		entry.remove_prefix(11);
247
		entry = entry.substr(0, entry.find('#'));
248
		StringOp::trim(entry, " \t");
249
		uint32_t ip = 0;
250
		if (parseIPv4String(entry, ip)) {
251
			if (n == 0) {
252
				dns1 = ip;
253
			} else {
254
				dns2 = ip;
255
			}
256
			++n;
257
		}
258
	}
259
}
260
261
// Default gateway (Linux): /proc/net/route, first default (00000000)
262
// route. The address is printed as a little-endian hex value.
263
static void readDefaultGateway(HostNetInfo& info)
264
{
265
	std::ifstream route("/proc/net/route");
266
	std::string line;
267
	while (std::getline(route, line)) {
268
		if (line.starts_with("Iface")) continue; // header
269
		std::istringstream iss(line);
270
		std::string iface;
271
		std::string dest;
272
		std::string gw;
273
		std::string flags;
274
		iss >> iface >> dest >> gw >> flags;
275
		if (dest != "00000000") continue; // not the default route
276
		// flags bit 0 = RTF_UP: the route is usable
277
		if (auto f = StringOp::stringToBase<16, unsigned long>(flags);
278
		    !f.has_value() || !(*f & 1)) continue;
279
		auto g = StringOp::stringToBase<16, unsigned long>(gw);
280
		if (!g.has_value()) continue;
281
		info.gateway = ntohl(*g);
282
		break;
283
	}
284
}
285
286
// Preference score for a network interface: wired > wireless > other
287
// physical > virtual (docker, veth, tun/tap, VPN, ...). Negative scores
288
// are never used; virtual interfaces are kept as a fallback so machines
289
// that only have one still work.
290
static int interfaceScore(std::string_view name, unsigned flags)
291
{
292
	if (!(flags & IFF_UP)) return -1;
293
	if (flags & IFF_LOOPBACK) return -1;
294
	static constexpr auto virtualPrefixes = std::to_array<std::string_view>({
295
		"docker", "veth", "virbr", "br-", "tun", "tap",
296
		"utun", "awdl", "bridge", "tailscale", "zt", "wg",
297
		"vmnet", "vbox"
298
	});
299
	for (auto& prefix : virtualPrefixes) {
300
		if (name.starts_with(prefix)) return 0;
301
	}
302
	if (name.starts_with("en") || name.starts_with("eth")) return 3; // wired
303
	if (name.starts_with("wl") || name.starts_with("wlan") ||
304
	    name.starts_with("ra")) return 2; // wireless
305
	return 1; // other physical
306
}
307
308
static std::optional<HostNetInfo> getHostNetInfo()
309
{
310
	struct ifaddrs* ifa = nullptr;
311
	if (getifaddrs(&ifa) != 0) return std::nullopt;
312
313
	// Pick the preferred interface: the highest-scoring one with a usable
314
	// IPv4 address (ties keep the first one).
315
	int bestScore = -1;
316
	std::optional<HostNetInfo> bestInfo;
317
	for (auto* it = ifa; it; it = it->ifa_next) {
318
		if (!it->ifa_addr || it->ifa_addr->sa_family != AF_INET) continue;
319
		int score = interfaceScore(it->ifa_name, it->ifa_flags);
320
		if (score <= bestScore) continue; // ties keep the first one
321
		auto* sa = std::bit_cast<const sockaddr_in*>(it->ifa_addr);
322
		uint32_t ip = ntohl(sa->sin_addr.s_addr);
323
		if (ip == 0) continue;
324
		HostNetInfo info;
325
		info.ip = ip;
326
		if (it->ifa_netmask) {
327
			auto* sm = std::bit_cast<const sockaddr_in*>(it->ifa_netmask);
328
			info.netmask = ntohl(sm->sin_addr.s_addr);
329
		}
330
		bestScore = score;
331
		bestInfo = std::move(info);
332
	}
333
	freeifaddrs(ifa);
334
	if (!bestInfo) return std::nullopt;
335
336
#ifndef __APPLE__
337
	readDefaultGateway(*bestInfo);
338
#endif
339
340
	readResolvConfDns(bestInfo->dns1, bestInfo->dns2);
341
	return bestInfo;
342
}
343
#endif
344
345
// Ephemeral port selection (spec 4.4.1): random port in 16384-32767.
346
// Device-local generator: openMSX's random_int() helpers share a global
347
// generator that is not thread-safe against the emulation thread, and
348
// this code runs on the ESP thread.
349
static uint16_t randomEphemeralPort()
350
{
351
	static std::mt19937 rng(std::random_device{}());
352
	static std::uniform_int_distribution dist(16384, 32767);
353
	return static_cast<uint16_t>(dist(rng));
354
}
355
356
// ---- connection structure ----
357
358
// Passive TCP listener registry entry: multiple passive connections may
359
// share a listener for the same local port. The socket is owned by the
360
// shared ListenSocket object and closed by its destructor when the last
361
// referencing connection is freed.
362
struct EmulatedEsp32::ListenSocket {
363
	SOCKET sock = OPENMSX_INVALID_SOCKET;
364
	uint16_t port = 0;
365
	~ListenSocket() {
366
		if (sock != OPENMSX_INVALID_SOCKET) {
367
			sock_close(sock);
368
		}
369
	}
370
};
371
372
class EmulatedEsp32::Connection {
373
public:
374
	// The enclosing class manages connections (spawning/joining the
375
	// reader threads and tearing down sockets), so it needs access to
376
	// all state.
377
	friend class EmulatedEsp32;
378
379
	// Received-data buffer, guarded by recvMutex: all access goes through
380
	// the methods below.
381
	void pushRecv(std::span<const uint8_t> data) {
382
		std::scoped_lock lock(recvMutex);
383
		recvBuffer.push_back_range(data);
384
	}
385
	[[nodiscard]] size_t recvAvail() const {
386
		std::scoped_lock lock(recvMutex);
387
		return recvBuffer.size();
388
	}
389
	// Copies up to 'max' bytes into 'out' (the content of 'out' is
390
	// replaced) and removes them from the buffer.
391
	void popRecv(std::vector<uint8_t>& out, size_t max) {
392
		std::scoped_lock lock(recvMutex);
393
		size_t toRead = std::min(recvBuffer.size(), max);
394
		out.assign(recvBuffer.begin(), recvBuffer.begin() + toRead);
395
		recvBuffer.remove_prefix(toRead);
396
	}
397
	void clearRecv() {
398
		std::scoped_lock lock(recvMutex);
399
		recvBuffer.clear();
400
	}
401
402
private:
403
	int type = 0; // 0=free, 1=TCP, 2=UDP
404
	SOCKET sock = OPENMSX_INVALID_SOCKET;
405
406
	// Passive TCP: the (possibly shared) listener socket and the accepted
407
	// client socket. clientSock stays open after the remote side closes so
408
	// that SEND/RCV/STATE keep reporting a connected (dead) connection,
409
	// like the firmware's ClientList.
410
	SOCKET listenSock = OPENMSX_INVALID_SOCKET;
411
	SOCKET clientSock = OPENMSX_INVALID_SOCKET;
412
	std::shared_ptr<ListenSocket> listen; // the shared listener (passive)
413
	std::atomic<bool> clientEof{false}; // accepted client closed remotely
414
415
	uint32_t remoteIP = 0;
416
	uint16_t remotePort = 0;
417
	uint16_t localPort = 0;
418
	uint8_t flags = 0;
419
	std::atomic<uint8_t> state{0}; // 0=closed, 1=opening, 2=open, 3=closing
420
421
	// TLS (TCP active connections, TCP-IP UNAPI spec 1.1): opaque OpenSSL
422
	// session. The handshake runs in the reader thread; the connection is
423
	// only considered ESTABLISHED once it has completed (spec: TCP_STATE
424
	// must not report ESTABLISHED before the handshake is finished).
425
	std::optional<OpenSSL::SessionHandle> ssl;  // null when not using TLS
426
	bool tlsVerify = false;     // validate the server certificate
427
	std::atomic<uint8_t> handshakePhase{0}; // 0=no TLS, 1=handshaking, 2=done
428
429
	std::thread readerThread;
430
	std::atomic<bool> readerActive{false};
431
432
	cb_queue<uint8_t> recvBuffer;
433
	mutable std::mutex recvMutex;
434
};
435
436
// ====================================================================
437
//  Construction / destruction
438
// ====================================================================
439
440
EmulatedEsp32::EmulatedEsp32(const DeviceConfig& config, std::function<void(std::span<const uint8_t>)> sink_)
441
	: enabledSetting(
442
		config.getCommandController(), "smxwifi-emulatedesp32-link-enabled",
443
		"Report the emulated ESP32 WiFi link as up (UNAPI NET_STATE)", true)
444
	, cliComm(config.getCliComm())
445
	, sink(std::move(sink_))
446
{
447
	// Probe for a host-installed OpenSSL runtime; TLS support is
448
	// advertised and enabled only when one is found (the TLS capability
449
	// bits are not advertised otherwise, and TLS TCP_OPEN requests get
450
	// ERR_NOT_IMP).
451
	if (const auto* lib = OpenSSL::load()) {
452
		config.getCliComm().printInfo(
453
			"SMXWiFi: TLS support enabled (",
454
			lib->version(), ')');
455
	} else {
456
		config.getCliComm().printInfo(
457
			"SMXWiFi: OpenSSL not found, TLS support disabled "
458
			"(install OpenSSL to enable it)");
459
	}
460
461
	// Start the ESP emulation thread immediately.  Note that we can NOT
462
	// rely on powerUp() to do this: when the extension is inserted into an
463
	// already-running machine, powerUp() is never called on the new device.
464
	start();
465
}
466
467
EmulatedEsp32::~EmulatedEsp32()
468
{
469
	stop();
470
}
471
472
// ====================================================================
473
//  Reset
474
// ====================================================================
475
476
// Closes all network connections and clears the emulated device state
477
// (parser, boot sequence and pending input). Called by the owner on MSX
478
// reset and whenever it switches to or from this endpoint, so the
479
// emulation always starts clean after a migration.
480
void EmulatedEsp32::resetState()
481
{
482
	// Close all connections and stop reader threads
483
	{
484
		std::scoped_lock lock(connectionsMutex);
485
		for (auto& cp : connections) {
486
			if (!cp) continue;
487
			cp->readerActive = false;
488
			if (cp->readerThread.joinable()) {
489
				cp->readerThread.join();
490
			}
491
			cp->readerThread = std::thread();
492
			if (cp->ssl) {
493
				cp->ssl.reset();
494
			}
495
			if (cp->clientSock != OPENMSX_INVALID_SOCKET) {
496
				sock_close(cp->clientSock);
497
			}
498
			if (!cp->listen && cp->sock != OPENMSX_INVALID_SOCKET) {
499
				sock_close(cp->sock);
500
			}
501
			// Dropping 'cp' releases the shared listener; its socket is
502
			// closed by the destructor when the last connection goes.
503
			cp.reset();
504
		}
505
	}
506
	resetFifo();
507
	lastResponse.clear();
508
	// Wake the thread so a pending "Ready" boot retry is cancelled
509
	// promptly (resetParser() clears the boot stage).
510
	std::scoped_lock lock(msxToEspMutex);
511
	msxToEspCond.notify_one();
512
}
513
514
// ====================================================================
515
//  Serial contract (the owner routes to this endpoint when active)
516
// ====================================================================
517
518
void EmulatedEsp32::recvByte(uint8_t value, EmuTime /*time*/)
519
{
520
	std::scoped_lock lock(msxToEspMutex);
521
	msxToEspFifo.push_back(value);
522
	msxToEspCond.notify_one();
523
}
524
525
void EmulatedEsp32::setBaudRate(unsigned /*baud*/) const
526
{
527
	// The emulated firmware does not care about the UART baud rate.
528
}
529
530
// Clears the pending MSX->ESP input (UART FIFO reset command).
531
void EmulatedEsp32::resetFifo()
532
{
533
	std::scoped_lock lock(msxToEspMutex);
534
	msxToEspFifo.clear();
535
	msxToEspCond.notify_one();
536
}
537
538
// ====================================================================
539
//  Thread helpers
540
// ====================================================================
541
542
void EmulatedEsp32::start()
543
{
544
	if (espRunning.load()) return;
545
	espRunning = true;
546
	espThread = std::thread([this] { espThreadFunc(); });
547
}
548
549
void EmulatedEsp32::stop()
550
{
551
	if (!espRunning.load()) return;
552
	espRunning = false;
553
	{
554
		std::scoped_lock lock(msxToEspMutex);
555
		msxToEspCond.notify_one();
556
	}
557
	if (espThread.joinable()) {
558
		espThread.join();
559
	}
560
	// Stop any reader threads still running
561
	{
562
		std::scoped_lock lock(connectionsMutex);
563
		for (const auto& cp : connections) {
564
			if (!cp) continue;
565
			cp->readerActive = false;
566
			if (cp->readerThread.joinable()) {
567
				cp->readerThread.join();
568
			}
569
			cp->readerThread = std::thread();
570
			if (cp->ssl) {
571
				cp->ssl.reset();
572
			}
573
			if (cp->listen) {
574
				// Shared listener: dropping the reference closes the
575
				// socket when the last connection goes away.
576
				cp->listen.reset();
577
				cp->listenSock = OPENMSX_INVALID_SOCKET;
578
				cp->sock = OPENMSX_INVALID_SOCKET;
579
			} else if (cp->sock != OPENMSX_INVALID_SOCKET) {
580
				sock_close(cp->sock);
581
				cp->sock = OPENMSX_INVALID_SOCKET;
582
			}
583
		}
584
	}
585
}
586
587
// ====================================================================
588
//  ESP emulator thread  (reads MSX→ESP FIFO, processes commands)
589
// ====================================================================
590
591
// Boot sequence text to the MSX side (greeting, "Ready"): delivered
592
// as one block through the sink, like RS232Raw delivers bytes from the
593
// real ESP32 through the connector.
594
void EmulatedEsp32::pushBootText(std::string_view text) const
595
{
596
	sink(std::span<const uint8_t>(
597
		std::bit_cast<const uint8_t*>(text.data()), text.size()));
598
}
599
600
void EmulatedEsp32::resetParser()
601
{
602
	parserState = ParserState::IDLE;
603
	parserSizeStep = 0;
604
	parserExpectedSize = 0;
605
	parserDataBuf.clear();
606
}
607
608
// Dispatch a complete command. Must be called without msxToEspMutex held.
609
void EmulatedEsp32::processCommand()
610
{
611
	if (parserCmdByte < 63 || parserCmdByte >= 0x80) {
612
		handleUnapiCommand(parserCmdByte, parserDataBuf);
613
	} else {
614
		handleCustomCommand(parserCmdByte, parserDataBuf);
615
	}
616
	resetParser();
617
}
618
619
// Wait for work (FIFO data or shutdown) while the MSX->ESP FIFO is empty.
620
// Returns true when the thread should stop; on return 'unlocked' may hold
621
// work that must run without the mutex held.
622
bool EmulatedEsp32::espWaitForWork(std::unique_lock<std::mutex>& lock,
623
                                  std::function<void()>& unlocked)
624
{
625
	if (parserState == ParserState::IDLE) {
626
		if (bootStage == BootStage::NONE) {
627
			msxToEspCond.wait(lock, [this] {
628
				return !msxToEspFifo.empty() || !espRunning.load();
629
			});
630
			return !espRunning.load();
631
		}
632
		// "Ready" loop in progress: wake up on data, on shutdown, or when
633
		// the boot stage was cancelled (e.g. resetState() migrated the
634
		// endpoint away — a pending "Ready" retry must not fire after the
635
		// switch).
636
		bool timedOut = !msxToEspCond.wait_until(
637
			lock, bootEventTime, [this] {
638
			return !msxToEspFifo.empty() || !espRunning.load() ||
639
			       bootStage == BootStage::NONE;
640
		});
641
		if (!espRunning.load()) return true;
642
		if (timedOut) {
643
			unlocked = [this] { pushBootText("Ready\r\n"); };
644
			if (--bootReadyRetries == 0) {
645
				bootStage = BootStage::NONE;
646
			}
647
			bootEventTime = std::chrono::steady_clock::now() +
648
			                std::chrono::seconds(5);
649
		}
650
		return false;
651
	}
652
653
	// Waiting for the rest of a framed command: apply the firmware's
654
	// 250 ms inter-byte timeout.
655
	bool timedOut = !msxToEspCond.wait_until(
656
		lock, parserDeadline, [this] {
657
		return !msxToEspFifo.empty() || !espRunning.load();
658
	});
659
	if (!espRunning.load()) return true;
660
	if (timedOut) {
661
		resetParser();
662
	}
663
	return false;
664
}
665
666
// Parser state machine for one received byte, mirroring
667
// received_data_parser() in the ESP32 UNAPI firmware:
668
//  - unknown command bytes are discarded and the parser stays IDLE
669
//  - known quick commands execute immediately (no size header)
670
//  - known data commands expect CMD + 2-byte size (MSB first) + data
671
//  - a partial command not completed within 250 ms (no new byte
672
//    arriving) is discarded and the parser reverts to IDLE
673
// On return 'unlocked' may hold work that must run without the mutex held.
674
void EmulatedEsp32::espParseByte(uint8_t b, std::function<void()>& unlocked)
675
{
676
	switch (parserState) {
677
	case ParserState::IDLE:
678
		// Any byte received during the boot sequence stops it
679
		bootStage = BootStage::NONE;
680
		if (isQuickCustomCommand(b)) {
681
			// Quick custom command, no size header
682
			unlocked = [this, b] { handleCustomCommand(b, {}); };
683
			return;
684
		}
685
		if (isDataCommand(b)) {
686
			parserCmdByte = b;
687
			parserState = ParserState::WAIT_DATA_SIZE;
688
		}
689
		// Unknown command byte: discard, stay IDLE
690
		return;
691
692
	case ParserState::WAIT_DATA_SIZE:
693
		// 2-byte size, MSB first
694
		if (parserSizeStep == 0) {
695
			parserExpectedSize = static_cast<uint16_t>(b << 8);
696
			parserSizeStep = 1;
697
		} else {
698
			parserExpectedSize |= b;
699
			if (parserExpectedSize > MAX_CMD_DATA_LEN) {
700
				resetParser(); // invalid size, discard
701
			} else if (parserExpectedSize == 0) {
702
				// Size 0: process immediately, no data block
703
				unlocked = [this] { processCommand(); };
704
			} else {
705
				parserDataBuf.clear();
706
				parserState = ParserState::GET_DATA;
707
			}
708
			parserSizeStep = 0;
709
		}
710
		return;
711
712
	case ParserState::GET_DATA:
713
		parserDataBuf.push_back(b);
714
		if (parserDataBuf.size() >= parserExpectedSize) {
715
			unlocked = [this] { processCommand(); };
716
		}
717
		return;
718
	}
719
}
720
721
void EmulatedEsp32::espThreadFunc()
722
{
723
	// The parser and boot-sequence state used to be function-local; reset
724
	// it so a (re-)started thread begins from a clean state.
725
	resetParser();
726
	bootStage = BootStage::NONE;
727
	bootReadyRetries = 0;
728
729
	while (espRunning.load()) {
730
		// Work that must run without the mutex (command dispatch and
731
		// boot text pushes): decided under the lock, executed below.
732
		std::function<void()> unlocked;
733
		bool stop = false;
734
		{
735
			std::unique_lock lock(msxToEspMutex);
736
737
			if (bootStartPending) {
738
				// A reset was requested (cmdReset just ran): discard any
739
				// pending input — the real device loses its serial buffer
740
				// on reboot — and start the boot sequence (greeting
741
				// immediately after "R0").
742
				bootStartPending = false;
743
				resetParser();
744
				msxToEspFifo.clear();
745
				unlocked = [this] { pushBootText(bootGreeting); };
746
				bootStage = BootStage::READY_LOOP;
747
				bootReadyRetries = 3;
748
				bootEventTime = std::chrono::steady_clock::now() +
749
				                std::chrono::seconds(5);
750
			} else if (msxToEspFifo.empty()) {
751
				stop = espWaitForWork(lock, unlocked);
752
			} else {
753
				uint8_t b = msxToEspFifo.front();
754
				msxToEspFifo.pop_front();
755
				parserDeadline = std::chrono::steady_clock::now() +
756
				                 std::chrono::milliseconds(250);
757
				espParseByte(b, unlocked);
758
			}
759
		}
760
		if (stop) break;
761
		if (unlocked) unlocked();
762
	}
763
}
764
765
// ====================================================================
766
//  TCP reader thread  (reads inbound data from a connected TCP socket)
767
// ====================================================================
768
769
void EmulatedEsp32::tcpReaderThreadFunc(int connIdx)
770
{
771
	if (connIdx < 0 || connIdx >= MAX_CONNECTIONS) return;
772
773
	Connection* conn = nullptr;
774
	{
775
		std::scoped_lock lock(connectionsMutex);
776
		conn = connections[connIdx].get();
777
		if (!conn) return;
778
	}
779
780
	// The sockets are non-blocking. recv()/accept() are gated behind
781
	// select() so that a "would block" condition can never be mistaken for
782
	// a close from the remote side (on Windows sock_recv() maps
783
	// WSAEWOULDBLOCK to -1). Once select() reports a socket readable,
784
	// recv() only returns data or EOF; EOF sets state=3, which cmdTcpState()
785
	// reports as lwIP state CLOSE_WAIT, exactly like the real firmware.
786
	//
787
	// For passive connections this thread also performs the accept():
788
	// when the (possibly shared) listener reports a pending connection,
789
	// it is accepted and attached to this connection if none is attached
790
	// yet; otherwise it is immediately closed (dropped), like the
791
	// firmware's accept loop does when a client is already attached.
792
	//
793
	// For TLS connections this thread also drives the handshake (SSL_connect
794
	// needs both read and write readiness); the connection reports
795
	// SYN-SENT until the handshake and certificate validation have
796
	// finished (spec 4.5.5). A failed handshake stores the close reason
797
	// and reports the connection as failed (state=4), so TCP_STATE returns
798
	// ERR_NO_CONN with the reason, as the spec recommends.
799
	while (conn->readerActive.load()) {
800
		if (!tcpPollConnection(conn, connIdx)) break;
801
	}
802
}
803
804
// Polls the connection's sockets (never blocks) and handles whatever is
805
// ready. Returns false when the reader loop must stop (the remote side
806
// closed the connection, or TLS failed).
807
bool EmulatedEsp32::tcpPollConnection(Connection* conn, int connIdx)
808
{
809
	fd_set rfds;
810
	fd_set wfds;
811
	FD_ZERO(&rfds);
812
	FD_ZERO(&wfds);
813
	SOCKET s = (conn->listenSock != OPENMSX_INVALID_SOCKET)
814
	         ? conn->listenSock : conn->sock;
815
	bool inHandshake = (conn->handshakePhase.load() == 1);
816
	FD_SET(s, &rfds);
817
	if (inHandshake) {
818
		// The TLS handshake also needs to send (ClientHello, ...)
819
		FD_SET(s, &wfds);
820
	}
821
	bool haveClient = (conn->clientSock != OPENMSX_INVALID_SOCKET) &&
822
	                  !conn->clientEof.load();
823
	if (haveClient) {
824
		FD_SET(conn->clientSock, &rfds);
825
	}
826
	struct timeval tv = {5, 0}; // 5 ms poll interval (also bounds shutdown latency)
827
#ifdef _WIN32
828
	if (int sel = select(0, &rfds,
829
	                     inHandshake ? &wfds : nullptr, nullptr, &tv);
830
	    sel <= 0) {
831
#else
832
	auto maxFd = static_cast<int>(s);
833
	if (haveClient) {
834
		maxFd = std::max(maxFd, static_cast<int>(conn->clientSock));
835
	}
836
	if (int sel = select(maxFd + 1, &rfds,
837
	                     inHandshake ? &wfds : nullptr, nullptr, &tv);
838
	    sel <= 0) {
839
#endif
840
		// Nothing ready (or a transient select error): the 5 ms timeout
841
		// makes this a bounded re-check of the shutdown flag.
842
		return true;
843
	}
844
	if (FD_ISSET(s, &rfds) || (inHandshake && FD_ISSET(s, &wfds))) {
845
		if (conn->listenSock != OPENMSX_INVALID_SOCKET) {
846
			tcpAcceptClient(conn);
847
		} else if (inHandshake) {
848
			tcpDriveHandshake(conn, connIdx);
849
		} else if (tcpReadData(conn, connIdx)) {
850
			// Remote side closed the connection or TLS failed:
851
			// stop polling this connection.
852
			return false;
853
		}
854
	}
855
	if (haveClient && FD_ISSET(conn->clientSock, &rfds)) {
856
		tcpReadClient(conn);
857
	}
858
	if (conn->state == 4) {
859
		// TLS handshake or read failed: stop polling this connection
860
		// (the failure is reported by TCP_STATE via closeReason).
861
		return false;
862
	}
863
	return true;
864
}
865
866
// Passive: a connection request is pending on the (possibly shared)
867
// listener. Accept and attach it if none is attached yet; otherwise drop
868
// it immediately (firmware behavior).
869
void EmulatedEsp32::tcpAcceptClient(Connection* conn) const
870
{
871
	SOCKET ns = accept(conn->listenSock, nullptr, nullptr);
872
	if (ns == OPENMSX_INVALID_SOCKET) return;
873
	if (conn->clientSock != OPENMSX_INVALID_SOCKET) {
874
		// A client is already attached: drop the pending connection
875
		sock_close(ns);
876
		return;
877
	}
878
	// No client attached yet: attach this one
879
	sock_setNonBlocking(ns);
880
	// macOS/BSD only: suppress SIGPIPE when sending to a peer that reset
881
	// the connection (Linux uses MSG_NOSIGNAL instead). Not inherited by
882
	// accept().
883
#ifdef SO_NOSIGPIPE
884
	sock_setIntOption(ns, SOL_SOCKET, SO_NOSIGPIPE);
885
#endif
886
	conn->clientSock = ns;
887
	conn->clientEof = false;
888
	struct sockaddr_in peer{};
889
	if (::socklen_t peerLen = sizeof(peer);
890
	    getpeername(ns,
891
	        std::bit_cast<struct sockaddr*>(&peer),
892
	        &peerLen) == 0) {
893
		conn->remoteIP = ntohl(peer.sin_addr.s_addr);
894
		conn->remotePort = ntohs(peer.sin_port);
895
	}
896
	conn->state = 2; // open -> ESTABLISHED
897
}
898
899
// The generic openMSX socket helpers use char buffers (BSD sockets), while
900
// the SMXWiFi code is byte-oriented (uint8_t). Keep the char <-> uint8_t
901
// conversions in these wrappers, instead of at every call site.
902
[[nodiscard]] static ptrdiff_t sockRecv(SOCKET sd, std::span<uint8_t> buf)
903
{
904
	return sock_recv(sd, std::bit_cast<char*>(buf.data()), buf.size());
905
}
906
907
[[nodiscard]] static ptrdiff_t sockSend(SOCKET sd, std::span<const uint8_t> buf)
908
{
909
	return sock_send(sd, std::bit_cast<const char*>(buf.data()), buf.size());
910
}
911
912
[[nodiscard]] static int sockSendTo(SOCKET sd, std::span<const uint8_t> buf,
913
                                    const struct sockaddr* dest, ::socklen_t destLen)
914
{
915
	return sendto(sd, std::bit_cast<const char*>(buf.data()),
916
	              static_cast<int>(buf.size()), 0, dest, destLen);
917
}
918
919
[[nodiscard]] static int sockRecvFrom(SOCKET sd, std::span<uint8_t> buf,
920
                                      struct sockaddr* from, ::socklen_t* fromLen)
921
{
922
	return recvfrom(sd, std::bit_cast<char*>(buf.data()),
923
	                static_cast<int>(buf.size()), 0, from, fromLen);
924
}
925
926
// Active TLS: drive the handshake (the connection reports SYN-SENT until
927
// the handshake and certificate validation have finished).
928
void EmulatedEsp32::tcpDriveHandshake(Connection* conn, int connIdx)
929
{
930
	auto r = conn->ssl->handshake();
931
	if (!r) {
932
		if (conn->tlsVerify) {
933
			// Certificate validation failure (self-signed, expired,
934
			// hostname mismatch, ...): the specific reason is reported
935
			// by SSL_get_verify_result(), not by the error queue (which
936
			// only holds the generic "certificate verify failed" entry).
937
			uint8_t reason = static_cast<uint8_t>(conn->ssl->verifyResult());
938
			if (reason != 0) {
939
				cliComm.printWarning("SMXWiFi: TLS failure: certificate verification: ",
940
				                     conn->ssl->verifyErrorDescription());
941
				tcpTlsFail(conn, connIdx, reason);
942
				return;
943
			}
944
		}
945
		// Other handshake failure (protocol error, peer reset, ...):
946
		// report a TLS error like the firmware (reason 19)
947
		logTlsError(r.error());
948
		tcpTlsFail(conn, connIdx, 19);
949
	} else if (*r == OpenSSL::HandshakeType::Done) {
950
		// Handshake finished. With SSL_VERIFY_PEER a certificate
951
		// validation failure aborts the handshake itself, so success
952
		// here implies the certificate was accepted (spec 4.5.5: only
953
		// then the connection may report ESTABLISHED).
954
		conn->handshakePhase.store(2);
955
		conn->state = 2; // open -> ESTABLISHED
956
	}
957
	// NeedRead / NeedWrite: still waiting for the requested readiness
958
}
959
960
// Records a TLS failure: the close reason is sequenced before the state
961
// store, so TCP_STATE sees it whenever it observes state==4.
962
void EmulatedEsp32::tcpTlsFail(Connection* conn, int connIdx, uint8_t reason)
963
{
964
	closeReason[connIdx].store(reason);
965
	conn->state = 4; // failed (TLS) -> TCP_STATE reports ERR_NO_CONN
966
}
967
968
// Console diagnostics for TLS failures: the error code was captured at
969
// the moment of failure by OpenSSL::read()/write()/handshake(). No-op
970
// when the OpenSSL error queue was empty (e.g. a plain syscall failure).
971
void EmulatedEsp32::logTlsError(OpenSSL::ErrorCode code) const
972
{
973
	if (code.value == 0) return;
974
	cliComm.printWarning("SMXWiFi: TLS failure: ", OpenSSL::to_string(code));
975
}
976
977
// Active: read inbound data. Returns true when the reader loop must stop
978
// (the remote side closed the connection, or TLS failed).
979
bool EmulatedEsp32::tcpReadData(Connection* conn, int connIdx)
980
{
981
	std::array<uint8_t, 1024> buf;
982
	ptrdiff_t n = 0;
983
	if (conn->ssl) {
984
		// TLS: decrypt; a clean TLS close is a close_notify, a real
985
		// error fails the connection, WouldBlock is just "try again
986
		// later".
987
		auto r = conn->ssl->read(buf);
988
		if (!r) {
989
			auto& err = r.error();
990
			if (err) { // WouldBlock or Closed: no real error
991
				if (*err == OpenSSL::IoResult::Closed) {
992
					conn->state = 3; // remote closed -> CLOSE_WAIT
993
					return true;
994
				}
995
				// WouldBlock: nothing to do, loop again
996
				return false;
997
			}
998
			logTlsError(err.error());
999
			tcpTlsFail(conn, connIdx, 19); // TLS: other error
1000
			return true;
1001
		}
1002
		n = static_cast<ptrdiff_t>(*r);
1003
	} else {
1004
		n = sockRecv(conn->sock, buf);
1005
		if (n < 0) {
1006
			// Socket closed by the remote side (EOF) or error
1007
			conn->state = 3; // remote closed -> CLOSE_WAIT
1008
			return true;
1009
		}
1010
	}
1011
	if (n > 0) {
1012
		conn->pushRecv(std::span(buf).first(static_cast<size_t>(n)));
1013
		if (conn->ssl) {
1014
			tcpDrainSsl(conn, connIdx);
1015
		}
1016
	}
1017
	return false;
1018
}
1019
1020
// The SSL layer may have more plaintext buffered than one SSL_read
1021
// returns; drain it now (the socket itself would otherwise not be
1022
// selectable again).
1023
void EmulatedEsp32::tcpDrainSsl(Connection* conn, int connIdx)
1024
{
1025
	std::array<uint8_t, 1024> buf;
1026
	for (;;) {
1027
		auto r = conn->ssl->read(buf);
1028
		if (!r) {
1029
			auto& err = r.error();
1030
			if (err) {
1031
				if (*err == OpenSSL::IoResult::Closed) {
1032
					conn->state = 3; // remote closed -> CLOSE_WAIT
1033
				}
1034
				// WouldBlock: nothing more to drain
1035
			} else {
1036
				logTlsError(err.error());
1037
				tcpTlsFail(conn, connIdx, 19);
1038
			}
1039
			return;
1040
		}
1041
		conn->pushRecv(std::span(buf).first(*r));
1042
	}
1043
}
1044
1045
// Accepted client socket: read inbound data. When the client closes the
1046
// connection it stays attached (like the firmware's ClientList) and the
1047
// connection reports CLOSE_WAIT; new pending connections keep being
1048
// dropped.
1049
void EmulatedEsp32::tcpReadClient(Connection* conn) const
1050
{
1051
	std::array<uint8_t, 1024> buf;
1052
	auto n = sockRecv(conn->clientSock, buf);
1053
	if (n < 0) {
1054
		conn->clientEof = true;
1055
		conn->state = 3; // remote closed -> CLOSE_WAIT
1056
		return;
1057
	}
1058
	conn->pushRecv(std::span(buf).first(static_cast<size_t>(n)));
1059
}
1060
1061
// ====================================================================
1062
//  Connection manager
1063
// ====================================================================
1064
1065
EmulatedEsp32::Connection* EmulatedEsp32::allocateConnection()
1066
{
1067
	for (int i = 0; i < MAX_CONNECTIONS; ++i) {
1068
		if (connections[i] && connections[i]->type != 0) continue;
1069
		if (!connections[i]) {
1070
			connections[i] = std::make_unique<Connection>();
1071
		} else {
1072
			resetConnectionSlot(i);
1073
		}
1074
		closeReason[i].store(0);
1075
		return connections[i].get();
1076
	}
1077
	return nullptr;
1078
}
1079
1080
// Resets a previously used connection slot to its fresh state.
1081
void EmulatedEsp32::resetConnectionSlot(int i)
1082
{
1083
	auto& c = *connections[i];
1084
	c.type = 0;
1085
	c.sock = OPENMSX_INVALID_SOCKET;
1086
	c.listenSock = OPENMSX_INVALID_SOCKET;
1087
	c.clientSock = OPENMSX_INVALID_SOCKET;
1088
	c.listen.reset();
1089
	c.clientEof = false;
1090
	c.clearRecv();
1091
	c.remoteIP = 0;
1092
	c.remotePort = 0;
1093
	c.localPort = 0;
1094
	c.flags = 0;
1095
	c.state = 0;
1096
	c.ssl.reset();
1097
	c.tlsVerify = false;
1098
	c.handshakePhase.store(0);
1099
	c.readerActive = false;
1100
}
1101
1102
void EmulatedEsp32::freeConnection(int idx)
1103
{
1104
	if (idx < 0 || idx >= MAX_CONNECTIONS) return;
1105
	const auto& cp = connections[idx];
1106
	if (!cp) return;
1107
	cp->readerActive = false;
1108
	if (cp->readerThread.joinable()) {
1109
		cp->readerThread.join();
1110
	}
1111
	cp->readerThread = std::thread();
1112
	// Tear down the TLS session first (best-effort close_notify, then
1113
	// free); afterwards the socket can be closed.
1114
	if (cp->ssl) {
1115
		cp->ssl.reset();
1116
	}
1117
	if (cp->clientSock != OPENMSX_INVALID_SOCKET) {
1118
		sock_close(cp->clientSock);
1119
		cp->clientSock = OPENMSX_INVALID_SOCKET;
1120
	}
1121
	if (cp->listen) {
1122
		// Passive connection: the listener socket is shared; dropping
1123
		// our reference closes it when the last connection goes away.
1124
		cp->listen.reset();
1125
		cp->listenSock = OPENMSX_INVALID_SOCKET;
1126
		cp->sock = OPENMSX_INVALID_SOCKET; // same shared socket
1127
	} else if (cp->sock != OPENMSX_INVALID_SOCKET) {
1128
		sock_close(cp->sock);
1129
		cp->sock = OPENMSX_INVALID_SOCKET;
1130
	}
1131
	cp->handshakePhase.store(0);
1132
	cp->tlsVerify = false;
1133
	cp->type = 0;
1134
	cp->state = 0;
1135
	cp->clientEof = false;
1136
	cp->clearRecv();
1137
}
1138
1139
EmulatedEsp32::Connection* EmulatedEsp32::getConnection(int idx) const
1140
{
1141
	if (idx < 0 || idx >= MAX_CONNECTIONS) return nullptr;
1142
	return connections[idx].get();
1143
}
1144
1145
int EmulatedEsp32::getFreeConnectionSlot() const
1146
{
1147
	for (int i = 0; i < MAX_CONNECTIONS; ++i) {
1148
		if (!connections[i] || connections[i]->type == 0) return i;
1149
	}
1150
	return -1;
1151
}
1152
1153
// ====================================================================
1154
//  Response helpers
1155
// ====================================================================
1156
1157
void EmulatedEsp32::saveLastResponse(std::span<const uint8_t> data)
1158
{
1159
	lastResponse.assign(data.begin(), data.end());
1160
}
1161
1162
void EmulatedEsp32::sendQuickResponse(uint8_t cmdByte, uint8_t errorCode)
1163
{
1164
	std::array<uint8_t, 2> resp = {cmdByte, errorCode};
1165
	saveLastResponse(resp);
1166
	sink(resp);
1167
}
1168
1169
void EmulatedEsp32::sendResponse(uint8_t cmdByte, uint8_t errorCode,
1170
                                std::span<const uint8_t> data)
1171
{
1172
	auto respSize = narrow<uint16_t>(data.size());
1173
	auto resp = concat(std::to_array<uint8_t>({cmdByte, errorCode,
1174
	                                           uint8_t(respSize >> 8),
1175
	                                           uint8_t(respSize)}),
1176
	                   data);
1177
1178
	saveLastResponse(resp);
1179
	sink(resp);
1180
}
1181
1182
void EmulatedEsp32::sendRawResponse(std::span<const uint8_t> data)
1183
{
1184
	saveLastResponse(data);
1185
	sink(data);
1186
}
1187
1188
// ====================================================================
1189
//  Command dispatch
1190
// ====================================================================
1191
1192
void EmulatedEsp32::handleCustomCommand(uint8_t cmd, std::span<const uint8_t> data)
1193
{
1194
	switch (cmd) {
1195
	case 'R': cmdReset(data); break;
1196
	case 'W': cmdWarmReset(data); break;
1197
	case '?': cmdQuery(data); break;
1198
	case 'V': cmdGetVersion(data); break;
1199
	case 'r': cmdRetry(data); break;
1200
	case 'S': cmdScanAP(data); break;
1201
	case 's': cmdScanResults(data); break;
1202
	case 'A': cmdConnectAP(data); break;
1203
	case 'g': cmdGetAPStatus(data); break;
1204
	case 'b': cmdGetBoard(data); break;
1205
	case 'B': case 'U': case 'u': case 'Z': case 'Y':
1206
	case 'z': case 'E': case 'I':
1207
		cmdFirmwareUpdate(cmd, data); break;
1208
	case 'N': case 'D': case 'O': case 'o':
1209
	case 'a': case 'H': case 'h':
1210
		sendQuickResponse(cmd, 0); break;
1211
	case 'Q': cmdGetSettings(data); break;
1212
	case 'c': case 'C': cmdAutoClock(cmd, data); break;
1213
	case 'T': cmdSetWiFiTimer(data); break;
1214
	case 'G': cmdGetDateTime(data); break;
1215
	default:
1216
		sendQuickResponse(cmd, 4); // ERR_INV_PARAM
1217
		break;
1218
	}
1219
}
1220
1221
void EmulatedEsp32::handleUnapiCommand(uint8_t cmd, std::span<const uint8_t> data)
1222
{
1223
	switch (cmd) {
1224
	case UNAPI_GET_INFO:     cmdGetInfo(data); break;
1225
	case TCPIP_GET_CAPAB:    cmdGetCapab(data); break;
1226
	case TCPIP_GET_IPINFO:   cmdGetIPInfo(data); break;
1227
	case TCPIP_NET_STATE:    cmdNetState(data); break;
1228
	case TCPIP_DNS_Q:        cmdDnsQ(data); break;
1229
	case TCPIP_UDP_OPEN:     cmdUdpOpen(data); break;
1230
	case TCPIP_UDP_CLOSE:    cmdUdpClose(data); break;
1231
	case TCPIP_UDP_STATE:    cmdUdpState(data); break;
1232
	case TCPIP_UDP_SEND:     cmdUdpSend(data); break;
1233
	case TCPIP_UDP_RCV:      cmdUdpRcv(data); break;
1234
	case TCPIP_TCP_OPEN:     cmdTcpOpen(data); break;
1235
	case TCPIP_TCP_CLOSE:    cmdTcpClose(data); break;
1236
	case TCPIP_TCP_ABORT:    cmdTcpAbort(data); break;
1237
	case TCPIP_TCP_STATE:    cmdTcpState(data); break;
1238
	case TCPIP_TCP_SEND:     cmdTcpSend(data); break;
1239
	case TCPIP_TCP_RCV:      cmdTcpRcv(data); break;
1240
	case TCPIP_CONFIG_AUTOIP: cmdCfgAutoIP(data); break;
1241
	case TCPIP_CONFIG_IP:     cmdCfgIP(data); break;
1242
	case TCPIP_DNS_Q_NEW:     cmdDnsQNew(data); break;
1243
	// Unimplemented command codes of the UNAPI spec also fall here:
1244
	// 4 (SEND_ECHO), 5 (RCV_ECHO), 7 (legacy DNS, driver use improved 206 instead), 
1245
	// 19 to 24 (DISCARD and RAW IP functions) and 27 to 29 (TTL/PING CFG/WAIT)
1246
	default:
1247
		unimplementedCmd(cmd);
1248
		break;
1249
	}
1250
}
1251
1252
// ====================================================================
1253
//  Helper: parse IP address string → 4 bytes
1254
// ====================================================================
1255
1256
bool EmulatedEsp32::parseIP(const std::string& str, std::span<uint8_t, 4> ipOut) const
1257
{
1258
	uint32_t ip;
1259
	if (!parseIPv4String(str, ip)) return false;
1260
	Endian::write_UA_B32(ipOut.data(), ip);
1261
	return true;
1262
}
1263
1264
// ====================================================================
1265
//  Custom command implementations
1266
// ====================================================================
1267
1268
void EmulatedEsp32::cmdReset(std::span<const uint8_t> /*data*/)
1269
{
1270
	// Close all connections
1271
	{
1272
		std::scoped_lock lock(connectionsMutex);
1273
		for (int i = 0; i < MAX_CONNECTIONS; ++i) {
1274
			freeConnection(i);
1275
		}
1276
	}
1277
	lastResponse.clear();
1278
	sendQuickResponse('R', '0');
1279
	// Emulate the device reboot: the ESP thread will wipe pending input
1280
	// and run the boot sequence (greeting + "Ready" loop).
1281
	bootStartPending = true;
1282
}
1283
1284
void EmulatedEsp32::cmdWarmReset(std::span<const uint8_t> /*data*/)
1285
{
1286
	{
1287
		std::scoped_lock lock(connectionsMutex);
1288
		for (int i = 0; i < MAX_CONNECTIONS; ++i) {
1289
			freeConnection(i);
1290
		}
1291
	}
1292
	lastResponse.clear();
1293
	// Raw text response, exactly like the firmware's Serial.println("Ready")
1294
	static constexpr auto msg = std::to_array<uint8_t>({'R', 'e', 'a', 'd', 'y', '\r', '\n'});
1295
	sendRawResponse(msg);
1296
}
1297
1298
void EmulatedEsp32::cmdQuery(std::span<const uint8_t> /*data*/)
1299
{
1300
	// Raw text response (no framing — the MSX expects "OK")
1301
	static constexpr auto ok = std::to_array<uint8_t>({'O', 'K'});
1302
	sendRawResponse(ok);
1303
}
1304
1305
void EmulatedEsp32::cmdGetVersion(std::span<const uint8_t> /*data*/)
1306
{
1307
	// Raw bytes, no framing: 'V' + major + minor as HEX values (not
1308
	// ASCII) — the firmware writes chVer[0]-'0' and chVer[2]-'0'
1309
	static constexpr auto resp = std::to_array<uint8_t>({'V', 0x01, 0x00});
1310
	sendRawResponse(resp);
1311
}
1312
1313
void EmulatedEsp32::cmdRetry(std::span<const uint8_t> /*data*/)
1314
{
1315
	if (lastResponse.empty()) {
1316
		sendQuickResponse('r', 4); // no previous response
1317
		return;
1318
	}
1319
	sink(lastResponse);
1320
}
1321
1322
void EmulatedEsp32::cmdScanAP(std::span<const uint8_t> /*data*/)
1323
{
1324
	sendQuickResponse('S', 0);
1325
}
1326
1327
void EmulatedEsp32::cmdScanResults(std::span<const uint8_t> /*data*/)
1328
{
1329
	constexpr uint8_t SCAN_NO_NETWORKS = 2;
1330
	sendQuickResponse('s', SCAN_NO_NETWORKS);
1331
}
1332
1333
void EmulatedEsp32::cmdConnectAP(std::span<const uint8_t> /*data*/)
1334
{
1335
	constexpr uint8_t ERR_OK = 0;
1336
	sendQuickResponse('A', ERR_OK);
1337
}
1338
1339
void EmulatedEsp32::cmdGetAPStatus(std::span<const uint8_t> /*data*/)
1340
{
1341
	static constexpr uint8_t statusConnected = 5; // Connected and got IP
1342
	static constexpr zstring_view apName = "SMXWiFi";
1343
	auto apNameLen = narrow<uint8_t>(apName.size());
1344
	uint16_t respSize = 1 + apNameLen + 1; // status + name + null
1345
1346
	std::vector<uint8_t> resp;
1347
	resp.reserve(respSize);
1348
	resp.push_back(statusConnected);
1349
	resp.insert(resp.end(), apName.begin(), apName.end());
1350
	resp.push_back(0); // null terminator
1351
1352
	sendResponse('g', 0, resp);
1353
}
1354
1355
void EmulatedEsp32::cmdGetBoard(std::span<const uint8_t> /*data*/)
1356
{
1357
	// Board identification, like the firmware's FIRMWARETYPE ('b' GETBOARD
1358
	// command): 'UN32OMSX' identifies the openMSX emulated ESP32.
1359
	static constexpr auto boardId = std::to_array<uint8_t>({
1360
		'U', 'N', '3', '2', 'O', 'M', 'S', 'X', 0
1361
	});
1362
	sendResponse('b', 0, boardId);
1363
}
1364
1365
void EmulatedEsp32::cmdFirmwareUpdate(uint8_t cmd, std::span<const uint8_t> /*data*/)
1366
{
1367
	// All firmware / certificate update commands fail in the emulator.
1368
	// Error codes mirror the firmware's failure paths:
1369
	//  - 'B' (FILE_BOARD): FIRMWARETYPE mismatch
1370
	//  - 'U'/'u' (UPDATE_FW / UPDATE_CERTS): OTA failure
1371
	//  - 'Z'/'Y' (START_RS232_*): wrong data length (12 bytes required)
1372
	//  - 'z' (BLOCK_RS232_UPDATE): no update in progress
1373
	//  - 'E' (END_RS232_UPDATE): no update in progress
1374
	//  - 'I' (INITCERTS): certificate store unavailable
1375
	uint8_t error = 4; // UNAPI_ERR_INV_PARAM
1376
	if (cmd == 'I') error = 3; // UNAPI_ERR_NO_DATA
1377
	sendQuickResponse(cmd, error);
1378
}
1379
1380
void EmulatedEsp32::cmdGetSettings(std::span<const uint8_t> /*data*/)
1381
{
1382
	// Framed response, like the firmware's
1383
	// SendResponse(CUSTOM_F_QUERY_SETTINGS, OK, len, "OFF:30")
1384
	static constexpr auto settings = std::to_array<uint8_t>({
1385
		'O', 'F', 'F', ':', '3', '0'
1386
	});
1387
	sendResponse('Q', 0, settings);
1388
}
1389
1390
void EmulatedEsp32::cmdAutoClock(uint8_t cmd, std::span<const uint8_t> data)
1391
{
1392
	if (cmd == 'c') {
1393
		// AutoClock=0 (off), GMT=0
1394
		std::array<uint8_t, 3> resp = {0, 0, 0};
1395
		sendResponse('c', 0, resp);
1396
		return;
1397
	}
1398
	// data: autoClock (0..3) + GMT offset byte
1399
	if (data.size() != 2 || data[0] > 3) {
1400
		sendQuickResponse('C', 4); // UNAPI_ERR_INV_PARAM
1401
		return;
1402
	}
1403
	sendQuickResponse('C', 0);
1404
}
1405
1406
void EmulatedEsp32::cmdSetWiFiTimer(std::span<const uint8_t> data)
1407
{
1408
	// data: radio-off timer, 2 bytes (MSB first)
1409
	if (data.size() != 2) {
1410
		sendQuickResponse('T', 4); // UNAPI_ERR_INV_PARAM
1411
		return;
1412
	}
1413
	sendQuickResponse('T', 0);
1414
}
1415
1416
void EmulatedEsp32::cmdGetDateTime(std::span<const uint8_t> /*data*/)
1417
{
1418
	// Return "no network" error since SNTP is not implemented
1419
	constexpr uint8_t ERR_NO_NETWORK = 2;
1420
	sendQuickResponse('G', ERR_NO_NETWORK);
1421
}
1422
1423
// ====================================================================
1424
//  UNAPI command implementations
1425
// ====================================================================
1426
1427
void EmulatedEsp32::cmdGetInfo(std::span<const uint8_t> /*data*/)
1428
{
1429
	// UNAPI_GET_INFO — not normally sent over serial.
1430
	// Return ERR_NOT_IMP as the MSX driver handles this locally.
1431
	unimplementedCmd(0);
1432
}
1433
1434
void EmulatedEsp32::unimplementedCmd(uint8_t cmd)
1435
{
1436
	// All UNAPI responses use the framed format (CMD, ERR, SIZE, data)
1437
	sendResponse(cmd, 1); // ERR_NOT_IMP
1438
}
1439
1440
void EmulatedEsp32::cmdGetCapab(std::span<const uint8_t> data)
1441
{
1442
	// Response blocks byte-match the ESP32 firmware: each block is
1443
	// prefixed by its own data size byte.
1444
	if (data.size() != 1 || data[0] == 0 || data[0] > 4) {
1445
		sendResponse(1, 4); // ERR_INV_PARAM
1446
		return;
1447
	}
1448
	uint8_t block = data[0];
1449
	switch (block) {
1450
	case 1: {
1451
		// Capability flags (2), feature flags (2), link level (1)
1452
		std::array<uint8_t, 5> resp = {0x2C, 0x44, 0x96, 0x1C, 0x04};
1453
		sendResponse(1, 0, resp);
1454
		break;
1455
	}
1456
	case 2: {
1457
		// Max TCP (1), max UDP (1), free TCP (1), free UDP (1),
1458
		// max raw TCP (1), max raw UDP (1)
1459
		std::array<uint8_t, 6> resp = {4, 4, 0, 0, 0, 0};
1460
		{
1461
			std::scoped_lock lock(connectionsMutex);
1462
			unsigned char used = 0;
1463
			for (const auto& cp : connections) {
1464
				if (cp && cp->type != 0) ++used;
1465
			}
1466
			resp[2] = 4 - used; // free TCP
1467
			resp[3] = 4 - used; // free UDP
1468
		}
1469
		sendResponse(1, 0, resp);
1470
		break;
1471
	}
1472
	case 3: {
1473
		// Max incoming datagram size (2 LSB MSB), max outgoing (2 LSB MSB).
1474
		// Compile-time constants left as explicit byte literals: the
1475
		// endianness is visible in the initializer itself (no runtime
1476
		// serialization to factor into Endian::write_UA_L16()).
1477
		std::array<uint8_t, 4> resp = {
1478
			1500 & 0xFF, (1500 >> 8) & 0xFF,
1479
			2048 & 0xFF, (2048 >> 8) & 0xFF
1480
		};
1481
		sendResponse(1, 0, resp);
1482
		break;
1483
	}
1484
	case 4: {
1485
		// Secondary capability flags (2), feature flags (2), unused (1)
1486
		// Bit 8 (use TLS in TCP active connections) is advertised only
1487
		// when a host OpenSSL runtime is available; bit 9 (TLS in passive
1488
		// connections) is never advertised.
1489
		std::array<uint8_t, 5> resp = {0xF7, 0x00, 0, 0, 0};
1490
		if (OpenSSL::load()) {
1491
			resp[1] |= 0x01; // bit 8: TLS in TCP active connections
1492
		}
1493
		sendResponse(1, 0, resp);
1494
		break;
1495
	}
1496
	default:
1497
		break;
1498
	}
1499
}
1500
1501
void EmulatedEsp32::cmdGetIPInfo(std::span<const uint8_t> data)
1502
{
1503
	if (data.size() != 1 || data[0] == 0 || data[0] > 6) {
1504
		sendResponse(2, 4); // ERR_INV_PARAM
1505
		return;
1506
	}
1507
	uint8_t idx = data[0];
1508
	std::array<uint8_t, 4> ipBytes{};
1509
	if (idx != 2) { // idx 2 = Peer IP, not applicable
1510
		// Report the host's real network configuration (like the ESP32
1511
		// reporting its WiFi IP); it is recomputed per query, so it tracks
1512
		// host interface changes.
1513
		uint32_t ip = 0;
1514
		if (auto net = getHostNetInfo()) {
1515
			switch (idx) {
1516
			case 1: ip = net->ip; break;
1517
			case 3: ip = net->netmask; break;
1518
			case 4: ip = net->gateway; break;
1519
			case 5: ip = net->dns1; break;
1520
			case 6: ip = net->dns2; break;
1521
			default: break;
1522
			}
1523
		}
1524
		// No usable host value: 0.0.0.0 for IP/netmask/gateway/DNS2.
1525
		// Primary DNS gets a mock (8.8.8.8): DNS lookups are performed on
1526
		// the host anyway, so the value is purely informational.
1527
		if (idx == 5 && ip == 0) ip = 0x08080808;
1528
		Endian::write_UA_B32(ipBytes.data(), ip);
1529
	}
1530
	sendResponse(2, 0, ipBytes);
1531
}
1532
1533
void EmulatedEsp32::cmdNetState(std::span<const uint8_t> data)
1534
{
1535
	if (!data.empty()) {
1536
		sendResponse(3, 4); // ERR_INV_PARAM
1537
		return;
1538
	}
1539
	uint8_t state = enabledSetting.getBoolean() ? 2 : 0; // 2=Open, 0=Closed
1540
	sendResponse(3, 0, {&state, 1});
1541
}
1542
1543
void EmulatedEsp32::cmdDnsQ(std::span<const uint8_t> data)
1544
{
1545
	// data: zero-terminated host name string
1546
	// Find null terminator
1547
	if (data.empty()) {
1548
		sendResponse(6, 4); // ERR_INV_PARAM
1549
		return;
1550
	}
1551
	// Build null-terminated string
1552
	std::string hostname(data.begin(), data.end());
1553
	// Remove trailing null if present
1554
	if (!hostname.empty() && hostname.back() == '\0') {
1555
		hostname.pop_back();
1556
	}
1557
1558
	struct addrinfo hints;
1559
	memset(&hints, 0, sizeof(hints));
1560
	hints.ai_family = AF_INET;
1561
	hints.ai_socktype = SOCK_STREAM;
1562
1563
	struct addrinfo* res = nullptr;
1564
	if (int err = getaddrinfo(hostname.c_str(), nullptr, &hints, &res);
1565
	    err != 0 || !res) {
1566
		if (res) freeaddrinfo(res);
1567
		sendResponse(6, 8); // ERR_DNS
1568
		return;
1569
	}
1570
1571
	auto* addr = std::bit_cast<const struct sockaddr_in*>(res->ai_addr);
1572
	uint32_t ip = ntohl(addr->sin_addr.s_addr);
1573
	std::array<uint8_t, 4> ipBytes;
1574
	Endian::write_UA_B32(ipBytes.data(), ip);
1575
	freeaddrinfo(res);
1576
1577
	sendResponse(6, 0, ipBytes);
1578
}
1579
1580
void EmulatedEsp32::cmdDnsQNew(std::span<const uint8_t> data)
1581
{
1582
	// data[0] = flags (only bits 0-2 are defined), data[1..] =
1583
	// zero-terminated host name string
1584
	if (data.size() < 2 || (data[0] & 0xf8)) {
1585
		sendResponse(206, 4); // ERR_INV_PARAM
1586
		return;
1587
	}
1588
	uint8_t flags = data[0];
1589
	auto hostnameSpan = data.subspan(1);
1590
	std::string hostname(hostnameSpan.begin(), hostnameSpan.end());
1591
	if (!hostname.empty() && hostname.back() == '\0') {
1592
		hostname.pop_back();
1593
	}
1594
1595
	// If the string is a valid IP address, use it directly
1596
	if (std::array<uint8_t, 4> ipBytes; parseIP(hostname, ipBytes)) {
1597
		sendResponse(206, 0, ipBytes);
1598
		return;
1599
	}
1600
	if (flags & 2) {
1601
		sendResponse(206, 6); // ERR_INV_IP
1602
		return;
1603
	}
1604
1605
	struct addrinfo hints;
1606
	memset(&hints, 0, sizeof(hints));
1607
	hints.ai_family = AF_INET;
1608
	hints.ai_socktype = SOCK_STREAM;
1609
1610
	struct addrinfo* res = nullptr;
1611
	if (int err = getaddrinfo(hostname.c_str(), nullptr, &hints, &res);
1612
	    err != 0 || !res) {
1613
		if (res) freeaddrinfo(res);
1614
		sendResponse(206, 8); // ERR_DNS
1615
		return;
1616
	}
1617
1618
	auto* addr = std::bit_cast<const struct sockaddr_in*>(res->ai_addr);
1619
	uint32_t ipN = ntohl(addr->sin_addr.s_addr);
1620
	std::array<uint8_t, 4> ipB;
1621
	Endian::write_UA_B32(ipB.data(), ipN);
1622
	freeaddrinfo(res);
1623
1624
	sendResponse(206, 0, ipB);
1625
}
1626
1627
void EmulatedEsp32::cmdUdpOpen(std::span<const uint8_t> data)
1628
{
1629
	// Parameters: localPort (2, LSB MSB) + transient flag (1)
1630
	if (data.size() != 3) {
1631
		sendResponse(8, 4); // ERR_INV_PARAM
1632
		return;
1633
	}
1634
	uint16_t localPort = Endian::read_UA_L16(data.data());
1635
	uint8_t lifetime = data[2]; // transient flag
1636
	(void)lifetime;
1637
1638
	if (localPort == 0 || lifetime > 1 ||
1639
	    (localPort >= 0xfff0 && localPort != 0xffff)) {
1640
		// Port 0 is unsupported and FFF0h-FFFEh are reserved (spec 4.4.1)
1641
		sendResponse(8, 4); // ERR_INV_PARAM
1642
		return;
1643
	}
1644
1645
	// FFFFh port: random port in 16384-32767, never a port number used by
1646
	// another open UDP connection (spec 4.4.1)
1647
	if (localPort == 0xFFFF) {
1648
		std::scoped_lock lock(connectionsMutex);
1649
		auto udpPortInUse = [this](uint16_t port) {
1650
			return std::ranges::any_of(connections, [&](const auto& cp) {
1651
				return cp && cp->type == 2 && cp->localPort == port;
1652
			});
1653
		};
1654
		do {
1655
			localPort = randomEphemeralPort();
1656
		} while (udpPortInUse(localPort));
1657
	}
1658
1659
	SOCKET s = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
1660
	if (s == OPENMSX_INVALID_SOCKET) {
1661
		sendResponse(8, 2); // ERR_NO_NETWORK
1662
		return;
1663
	}
1664
1665
	// Bind to local port
1666
	{
1667
		sockaddr_in bindAddr = sock_makeIPv4(0, localPort);
1668
		if (bind(s, std::bit_cast<struct sockaddr*>(&bindAddr),
1669
		         sizeof(bindAddr)) < 0) {
1670
			sock_close(s);
1671
			sendResponse(8, 10); // ERR_CONN_EXISTS
1672
			return;
1673
		}
1674
	}
1675
1676
	// Make socket non-blocking
1677
	sock_setNonBlocking(s);
1678
1679
	std::scoped_lock lock(connectionsMutex);
1680
	auto* conn = allocateConnection();
1681
	if (!conn) {
1682
		sock_close(s);
1683
		sendResponse(8, 9); // ERR_NO_FREE_CONN
1684
		return;
1685
	}
1686
	conn->type = 2; // UDP
1687
	conn->sock = s;
1688
	conn->localPort = localPort;
1689
	conn->state = 2; // OPEN
1690
1691
	int connIdx = 0;
1692
	for (int i = 0; i < MAX_CONNECTIONS; ++i) {
1693
		if (connections[i].get() == conn) {
1694
			connIdx = i;
1695
			break;
1696
		}
1697
	}
1698
	// Connection numbers are 1-based, as in the real firmware
1699
	auto connNum = static_cast<uint8_t>(connIdx + 1);
1700
	sendResponse(8, 0, {&connNum, 1});
1701
}
1702
1703
void EmulatedEsp32::cmdUdpClose(std::span<const uint8_t> data)
1704
{
1705
	if (data.size() != 1) {
1706
		sendResponse(9, 4); // ERR_INV_PARAM
1707
		return;
1708
	}
1709
	int num = data[0];
1710
	if (num > MAX_CONNECTIONS) {
1711
		sendResponse(9, 11); // ERR_NO_CONN
1712
		return;
1713
	}
1714
	std::scoped_lock lock(connectionsMutex);
1715
	if (num == 0) {
1716
		// Close all open (transient) UDP connections
1717
		for (int i = 0; i < MAX_CONNECTIONS; ++i) {
1718
			if (connections[i] && connections[i]->type == 2) {
1719
				freeConnection(i);
1720
			}
1721
		}
1722
		sendResponse(9, 0, {});
1723
		return;
1724
	}
1725
	if (const auto* conn = getConnection(num - 1);
1726
	    !conn || conn->type != 2) {
1727
		sendResponse(9, 11); // ERR_NO_CONN
1728
		return;
1729
	}
1730
	freeConnection(num - 1);
1731
	sendResponse(9, 0, {});
1732
}
1733
1734
void EmulatedEsp32::cmdUdpState(std::span<const uint8_t> data)
1735
{
1736
	if (data.size() != 1) {
1737
		sendResponse(10, 4); // ERR_INV_PARAM
1738
		return;
1739
	}
1740
	int num = data[0];
1741
	if (num == 0 || num > MAX_CONNECTIONS) {
1742
		sendResponse(10, 11); // ERR_NO_CONN
1743
		return;
1744
	}
1745
	std::scoped_lock lock(connectionsMutex);
1746
	const auto* conn = getConnection(num - 1);
1747
	if (!conn || conn->type != 2) {
1748
		sendResponse(10, 11); // ERR_NO_CONN
1749
		return;
1750
	}
1751
	// Return: local port (2), pending datagrams (1), oldest size (2)
1752
	uint16_t port = conn->localPort;
1753
	// Count pending datagrams is tricky with raw sockets — just return 0
1754
	uint8_t pendingDgrams = 0;
1755
	uint16_t oldestSize = 0;
1756
	// Built as a byte literal rather than Endian::write_UA_L16() calls:
1757
	// three fields of mixed width in one initializer, where the explicit
1758
	// byte order is self-documenting.
1759
	std::array<uint8_t, 5> resp = {
1760
		static_cast<uint8_t>(port & 0xFF),
1761
		static_cast<uint8_t>((port >> 8) & 0xFF),
1762
		pendingDgrams,
1763
		static_cast<uint8_t>(oldestSize & 0xFF),
1764
		static_cast<uint8_t>((oldestSize >> 8) & 0xFF)
1765
	};
1766
	sendResponse(10, 0, resp);
1767
}
1768
1769
void EmulatedEsp32::cmdUdpSend(std::span<const uint8_t> data)
1770
{
1771
	// conn num (1) + remote IP (4) + remote port (2) + payload
1772
	if (data.size() < 8) {
1773
		sendResponse(11, 4); // ERR_INV_PARAM
1774
		return;
1775
	}
1776
	int num = data[0];
1777
	if (num == 0 || num > MAX_CONNECTIONS) {
1778
		sendResponse(11, 11); // ERR_NO_CONN
1779
		return;
1780
	}
1781
	uint32_t destIP = (static_cast<uint32_t>(data[1]) << 24) |
1782
	                  (static_cast<uint32_t>(data[2]) << 16) |
1783
	                  (static_cast<uint32_t>(data[3]) << 8) |
1784
	                  static_cast<uint32_t>(data[4]);
1785
	uint16_t destPort = Endian::read_UA_L16(data.data() + 5);
1786
1787
	std::scoped_lock lock(connectionsMutex);
1788
	const auto* conn = getConnection(num - 1);
1789
	if (!conn) {
1790
		sendResponse(11, 11); // ERR_NO_CONN
1791
		return;
1792
	}
1793
	if (conn->type != 2) {
1794
		sendResponse(11, 2); // ERR_NO_NETWORK (firmware quirk)
1795
		return;
1796
	}
1797
1798
	sockaddr_in dest = sock_makeIPv4(destIP, destPort);
1799
1800
	auto payload = data.subspan(7);
1801
	if (int n = sockSendTo(conn->sock, payload,
1802
	                       std::bit_cast<struct sockaddr*>(&dest), sizeof(dest));
1803
	    n != static_cast<int>(payload.size())) {
1804
		sendResponse(11, 2); // ERR_NO_NETWORK
1805
		return;
1806
	}
1807
	sendResponse(11, 0, {});
1808
}
1809
1810
void EmulatedEsp32::cmdUdpRcv(std::span<const uint8_t> data)
1811
{
1812
	if (data.size() != 3) {
1813
		sendResponse(12, 4); // ERR_INV_PARAM
1814
		return;
1815
	}
1816
	int num = data[0];
1817
	if (num == 0 || num > MAX_CONNECTIONS) {
1818
		sendResponse(12, 11); // ERR_NO_CONN
1819
		return;
1820
	}
1821
	uint16_t maxSize = Endian::read_UA_L16(data.data() + 1);
1822
	if (maxSize > 2048) maxSize = 2048;
1823
1824
	std::scoped_lock lock(connectionsMutex);
1825
	const Connection* conn = getConnection(num - 1);
1826
	if (!conn || conn->type != 2) {
1827
		sendResponse(12, 11); // ERR_NO_CONN
1828
		return;
1829
	}
1830
1831
	// Non-blocking receive
1832
	std::vector<uint8_t> buf(maxSize > 0 ? maxSize : 2048);
1833
	struct sockaddr_in from{};
1834
	::socklen_t fromLen = sizeof(from);
1835
	int n = sockRecvFrom(conn->sock, buf,
1836
	                     std::bit_cast<struct sockaddr*>(&from), &fromLen);
1837
#ifdef _WIN32
1838
	// Winsock reports a datagram larger than the buffer as an error
1839
	// (WSAEMSGSIZE) after filling the buffer and consuming the datagram:
1840
	// that is a truncate-at-receive, not a failure. (POSIX recvfrom just
1841
	// fills the buffer.)
1842
	if (n < 0 && WSAGetLastError() == WSAEMSGSIZE) {
1843
		n = static_cast<int>(buf.size());
1844
	}
1845
#endif
1846
	if (n < 0) {
1847
		sendResponse(12, 3); // ERR_NO_DATA (includes 'would block')
1848
		return;
1849
	}
1850
1851
	uint32_t srcIP = ntohl(from.sin_addr.s_addr);
1852
	uint16_t srcPort = ntohs(from.sin_port);
1853
1854
	// Response: remote IP (4) + remote port (2 LSB MSB) + data
1855
	std::array<uint8_t, 6> hdr;
1856
	Endian::write_UA_B32(hdr.data(), srcIP);
1857
	Endian::write_UA_L16(hdr.data() + 4, srcPort);
1858
	std::vector<uint8_t> resp;
1859
	resp.reserve(6 + n);
1860
	resp.insert(resp.end(), hdr.begin(), hdr.end());
1861
	resp.insert(resp.end(), buf.begin(), buf.begin() + n);
1862
1863
	sendResponse(12, 0, resp);
1864
}
1865
1866
void EmulatedEsp32::cmdTcpOpen(std::span<const uint8_t> data)
1867
{
1868
	// Parameters (11+ bytes): remoteIP(4) + remotePort(2 LSB MSB)
1869
	//  + localPort(2 LSB MSB) + user timeout (2) + flags (1)
1870
	// flags: bit0 passive, bit1 resident, bit2 TLS, bit3 verify certs,
1871
	// bits 4-7 unused, must be zero (spec 4.5.1)
1872
	if (data.size() < 11) {
1873
		sendResponse(13, 4); // ERR_INV_PARAM
1874
		return;
1875
	}
1876
	uint32_t remoteIP = (static_cast<uint32_t>(data[0]) << 24) |
1877
	                    (static_cast<uint32_t>(data[1]) << 16) |
1878
	                    (static_cast<uint32_t>(data[2]) << 8) |
1879
	                    static_cast<uint32_t>(data[3]);
1880
	uint16_t remotePort = Endian::read_UA_L16(data.data() + 4);
1881
	uint16_t localPort = Endian::read_UA_L16(data.data() + 6);
1882
	uint16_t userTimeout = Endian::read_UA_L16(data.data() + 8);
1883
	uint8_t flags = data[10];
1884
1885
	// User timeout: 0 (default), 1-1080 (seconds) or FFFFh (infinite) are
1886
	// the only valid values (spec 4.5.1)
1887
	if (userTimeout != 0 && userTimeout != 0xFFFF &&
1888
	    (userTimeout < 1 || userTimeout > 1080)) {
1889
		sendResponse(13, 4); // ERR_INV_PARAM
1890
		return;
1891
	}
1892
	if (flags & 0xf0) {
1893
		sendResponse(13, 4); // ERR_INV_PARAM (unused flag set)
1894
		return;
1895
	}
1896
	if (localPort == 0) {
1897
		sendResponse(13, 4); // ERR_INV_PARAM
1898
		return;
1899
	}
1900
1901
	// The optional server host name (for TLS certificate validation)
1902
	// is passed inline, right after the fixed parameters block, like the
1903
	// ESP32 firmware reads it (spec 4.5.1 defines it as an MSX address,
1904
	// but the firmware takes it from the command data stream).
1905
	std::string hostname;
1906
	if (data.size() > 11) {
1907
		size_t len = std::min<size_t>(data.size() - 11, 255);
1908
		hostname.assign(data.begin() + 11, data.begin() + 11 + len);
1909
		if (auto nul = hostname.find('\0'); nul != std::string::npos) {
1910
			hostname.resize(nul);
1911
		}
1912
	}
1913
1914
	bool passive = (flags & 1) != 0;
1915
	bool useTls = (flags & 4) != 0;
1916
	bool verifyCert = (flags & 8) != 0;
1917
	if (passive) {
1918
		// Passive with a specified remote socket is not advertised
1919
		// (capability bit 4) and TLS in passive connections is not
1920
		// advertised (capability bit 9): both are ERR_NOT_IMP per spec
1921
		if (remoteIP != 0 || useTls) {
1922
			sendResponse(13, 1); // ERR_NOT_IMP
1923
			return;
1924
		}
1925
	} else {
1926
		// Active mode requires a remote address. TLS in active connections
1927
		// requires a host OpenSSL runtime (capability bit 8 is not
1928
		// advertised otherwise) -> ERR_NOT_IMP.
1929
		if (remoteIP == 0) {
1930
			sendResponse(13, 4); // ERR_INV_PARAM
1931
			return;
1932
		}
1933
		if (useTls && !OpenSSL::load()) {
1934
			sendResponse(13, 1); // ERR_NOT_IMP
1935
			return;
1936
		}
1937
	}
1938
	// Flag bit 3 (verify certificate) is only meaningful with TLS on an
1939
	// active connection; otherwise it is ignored (spec 4.5.1)
1940
1941
	// FFFFh local port: random port in 16384-32767, never a port number
1942
	// used by another open TCP connection (spec 4.5.1)
1943
	if (localPort == 0xFFFF) {
1944
		pickEphemeralTcpPort(localPort);
1945
	}
1946
1947
	if (passive) {
1948
		cmdTcpOpenPassive(localPort, flags);
1949
	} else {
1950
		cmdTcpOpenActive(remoteIP, remotePort, localPort, flags,
1951
		                 hostname, useTls, verifyCert);
1952
	}
1953
}
1954
1955
// Random ephemeral port (spec 4.5.1), never one used by another open
1956
// TCP connection.
1957
void EmulatedEsp32::pickEphemeralTcpPort(uint16_t& port)
1958
{
1959
	std::scoped_lock lock(connectionsMutex);
1960
	auto tcpPortInUse = [this](uint16_t p) {
1961
		return std::ranges::any_of(connections, [&](const auto& cp) {
1962
			return cp && cp->type == 1 && cp->localPort == p;
1963
		});
1964
	};
1965
	do {
1966
		port = randomEphemeralPort();
1967
	} while (tcpPortInUse(port));
1968
}
1969
1970
// Passive open: find an existing shared listener for this port, or
1971
// create a new one (passive connections with unspecified remote socket
1972
// may share a local port, spec 4.5.1); then register the connection and
1973
// start its reader thread.
1974
void EmulatedEsp32::cmdTcpOpenPassive(uint16_t localPort, uint8_t flags)
1975
{
1976
	std::scoped_lock lock(connectionsMutex);
1977
1978
	// Find an existing shared listener for this port, or create a new
1979
	// one (passive connections with unspecified remote socket may share
1980
	// a local port, spec 4.5.1). Expired registry entries are pruned.
1981
	std::shared_ptr<ListenSocket> entry;
1982
	std::erase_if(listenSockets, [](const auto& w) { return w.expired(); });
1983
	for (const auto& w : listenSockets) {
1984
		if (auto e = w.lock(); e && e->port == localPort) {
1985
			entry = std::move(e);
1986
			break;
1987
		}
1988
	}
1989
	if (!entry) {
1990
		SOCKET ls = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
1991
		if (ls == OPENMSX_INVALID_SOCKET) {
1992
			sendResponse(13, 2); // ERR_NO_NETWORK
1993
			return;
1994
		}
1995
		sock_setIntOption(ls, SOL_SOCKET, SO_REUSEADDR);
1996
		if (sockaddr_in bindAddr = sock_makeIPv4(0, localPort);
1997
		    bind(ls, std::bit_cast<struct sockaddr*>(&bindAddr),
1998
		         sizeof(bindAddr)) < 0) {
1999
			sock_close(ls);
2000
			sendResponse(13, 10); // ERR_CONN_EXISTS
2001
			return;
2002
		}
2003
		if (listen(ls, MAX_CONNECTIONS) < 0) {
2004
			sock_close(ls);
2005
			sendResponse(13, 10); // ERR_CONN_EXISTS
2006
			return;
2007
		}
2008
		sock_setNonBlocking(ls);
2009
		entry = std::make_shared<ListenSocket>();
2010
		entry->sock = ls;
2011
		entry->port = localPort;
2012
		listenSockets.push_back(entry);
2013
	}
2014
2015
	Connection* conn = allocateConnection();
2016
	if (!conn) {
2017
		// The local 'entry' reference goes away here: a freshly created
2018
		// listener is closed and its registry entry becomes expired.
2019
		sendResponse(13, 9); // ERR_NO_FREE_CONN
2020
		return;
2021
	}
2022
	int connIdx = 0;
2023
	for (int i = 0; i < MAX_CONNECTIONS; ++i) {
2024
		if (connections[i].get() == conn) {
2025
			connIdx = i;
2026
			break;
2027
		}
2028
	}
2029
	conn->type = 1; // TCP
2030
	conn->listen = entry;
2031
	conn->sock = entry->sock;
2032
	conn->listenSock = entry->sock;
2033
	conn->clientSock = OPENMSX_INVALID_SOCKET;
2034
	conn->clientEof = false;
2035
	conn->remoteIP = 0;
2036
	conn->remotePort = 0;
2037
	conn->localPort = localPort;
2038
	conn->flags = flags;
2039
	conn->state = 2; // open (LISTEN until a client is accepted)
2040
2041
	// Start reader thread (accepts clients and reads inbound data)
2042
	conn->readerActive = true;
2043
	conn->readerThread = std::thread(
2044
	        [this, connIdx] { tcpReaderThreadFunc(connIdx); });
2045
2046
	auto connNum = static_cast<uint8_t>(connIdx + 1);
2047
	sendResponse(13, 0, {&connNum, 1});
2048
}
2049
2050
// Active open: check that no TCP connection is already open with the
2051
// same combination of local port, remote IP and remote port (spec:
2052
// ERR_CONN_EXISTS), and that no passive listener owns the port; then
2053
// create the socket, connect, set up TLS and register the connection.
2054
void EmulatedEsp32::cmdTcpOpenActive(uint32_t remoteIP, uint16_t remotePort,
2055
                                     uint16_t localPort, uint8_t flags,
2056
                                     const std::string& hostname,
2057
                                     bool useTls, bool verifyCert)
2058
{
2059
	{
2060
		std::scoped_lock lock(connectionsMutex);
2061
		for (const auto& cp : connections) {
2062
			if (cp && cp->type == 1 && !cp->listen &&
2063
			    cp->localPort == localPort && cp->remoteIP == remoteIP &&
2064
			    cp->remotePort == remotePort) {
2065
				sendResponse(13, 10); // ERR_CONN_EXISTS
2066
				return;
2067
			}
2068
		}
2069
		for (const auto& w : listenSockets) {
2070
			if (auto e = w.lock(); e && e->port == localPort) {
2071
				sendResponse(13, 10); // ERR_CONN_EXISTS
2072
				return;
2073
			}
2074
		}
2075
		if (getFreeConnectionSlot() < 0) {
2076
			sendResponse(13, 9); // ERR_NO_FREE_CONN
2077
			return;
2078
		}
2079
	}
2080
2081
	SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
2082
	if (s == OPENMSX_INVALID_SOCKET) {
2083
		sendResponse(13, 2); // ERR_NO_NETWORK
2084
		return;
2085
	}
2086
2087
	// TCP_NODELAY
2088
	sock_setIntOption(s, IPPROTO_TCP, TCP_NODELAY);
2089
	// macOS/BSD only: suppress SIGPIPE when sending to a peer that reset
2090
	// the connection (Linux uses MSG_NOSIGNAL instead).
2091
#ifdef SO_NOSIGPIPE
2092
	sock_setIntOption(s, SOL_SOCKET, SO_NOSIGPIPE);
2093
#endif
2094
	sock_setIntOption(s, SOL_SOCKET, SO_REUSEADDR);
2095
2096
	// Bind local port (may fail if e.g. an OS-level socket owns it)
2097
	{
2098
		sockaddr_in bindAddr = sock_makeIPv4(0, localPort);
2099
		if (bind(s, std::bit_cast<struct sockaddr*>(&bindAddr),
2100
		         sizeof(bindAddr)) < 0) {
2101
			sock_close(s);
2102
			sendResponse(13, 10); // ERR_CONN_EXISTS
2103
			return;
2104
		}
2105
	}
2106
2107
	// Connect
2108
	if (sockaddr_in dest = sock_makeIPv4(remoteIP, remotePort);
2109
	    connect(s, std::bit_cast<struct sockaddr*>(&dest),
2110
	            sizeof(dest)) < 0) {
2111
		sock_close(s);
2112
		// Return error with close reason byte (unknown reason)
2113
		uint8_t reasonByte = 0;
2114
		sendResponse(13, 11, {&reasonByte, 1}); // ERR_NO_CONN
2115
		return;
2116
	}
2117
2118
	// Make socket non-blocking (reader thread polls for data)
2119
	sock_setNonBlocking(s);
2120
2121
	// TLS: create the OpenSSL session on the connected socket; the
2122
	// handshake itself runs in the reader thread (the connection reports
2123
	// SYN-SENT until it is finished, spec 4.5.5). The host name is used
2124
	// for SNI and, when verifying, for the certificate host name check.
2125
	std::optional<OpenSSL::SessionHandle> ssl;
2126
	if (useTls) {
2127
		ssl = OpenSSL::load()->createClientSession(
2128
			verifyCert, hostname, static_cast<int>(s));
2129
		if (!ssl) {
2130
			sock_close(s);
2131
			uint8_t reasonByte = 19; // TLS: other error
2132
			sendResponse(13, 11, {&reasonByte, 1}); // ERR_NO_CONN
2133
			return;
2134
		}
2135
	}
2136
2137
	{
2138
		std::scoped_lock lock(connectionsMutex);
2139
		auto* conn = allocateConnection();
2140
		// A free slot was checked before connecting; since all commands
2141
		// run on the same ESP thread, the slot is still free.
2142
		if (!conn) {
2143
			sock_close(s);
2144
			sendResponse(13, 9); // ERR_NO_FREE_CONN
2145
			return;
2146
		}
2147
		int connIdx = 0;
2148
		for (int i = 0; i < MAX_CONNECTIONS; ++i) {
2149
			if (connections[i].get() == conn) {
2150
				connIdx = i;
2151
				break;
2152
			}
2153
		}
2154
		conn->type = 1; // TCP
2155
		conn->sock = s;
2156
		conn->remoteIP = remoteIP;
2157
		conn->remotePort = remotePort;
2158
		conn->localPort = localPort;
2159
		conn->flags = flags;
2160
		conn->ssl = std::move(ssl);
2161
		conn->tlsVerify = verifyCert;
2162
		conn->handshakePhase = conn->ssl ? 1 : 0; // 1 = TLS handshake in progress
2163
		// Without TLS the TCP connect() already finished, so the
2164
		// connection is ESTABLISHED right away. With TLS it reports
2165
		// SYN-SENT (state=1) until the handshake and certificate
2166
		// validation have completed (spec 4.5.5: TCP_STATE must not
2167
		// report ESTABLISHED before the handshake is done).
2168
		conn->state = conn->ssl ? 1 : 2;
2169
2170
		// Start reader thread
2171
		conn->readerActive = true;
2172
		conn->readerThread = std::thread(
2173
		        [this, connIdx] { tcpReaderThreadFunc(connIdx); });
2174
2175
		auto connNum = static_cast<uint8_t>(connIdx + 1);
2176
		sendResponse(13, 0, {&connNum, 1});
2177
	}
2178
}
2179
2180
void EmulatedEsp32::cmdTcpClose(std::span<const uint8_t> data)
2181
{
2182
	if (data.size() != 1) {
2183
		sendResponse(14, 4); // ERR_INV_PARAM
2184
		return;
2185
	}
2186
	int num = data[0];
2187
	if (num > MAX_CONNECTIONS) {
2188
		sendResponse(14, 11); // ERR_NO_CONN
2189
		return;
2190
	}
2191
	std::scoped_lock lock(connectionsMutex);
2192
	if (num == 0) {
2193
		// Close all open transient TCP connections; resident connections
2194
		// (opened with flags bit 1 set) are kept
2195
		for (int i = 0; i < MAX_CONNECTIONS; ++i) {
2196
			if (connections[i] && connections[i]->type == 1 &&
2197
			    !(connections[i]->flags & 2)) {
2198
				freeConnection(i);
2199
			}
2200
		}
2201
		sendResponse(14, 0, {});
2202
		return;
2203
	}
2204
	auto* conn = getConnection(num - 1);
2205
	if (!conn || conn->type != 1) {
2206
		sendResponse(14, 11); // ERR_NO_CONN
2207
		return;
2208
	}
2209
	conn->state = 3; // closing
2210
	freeConnection(num - 1);
2211
	sendResponse(14, 0, {});
2212
}
2213
2214
void EmulatedEsp32::cmdTcpAbort(std::span<const uint8_t> data)
2215
{
2216
	if (data.size() != 1) {
2217
		sendResponse(15, 4); // ERR_INV_PARAM
2218
		return;
2219
	}
2220
	int num = data[0];
2221
	if (num > MAX_CONNECTIONS) {
2222
		sendResponse(15, 11); // ERR_NO_CONN
2223
		return;
2224
	}
2225
	std::scoped_lock lock(connectionsMutex);
2226
	if (num == 0) {
2227
		// Abort all open transient TCP connections; resident connections
2228
		// (opened with flags bit 1 set) are kept
2229
		for (int i = 0; i < MAX_CONNECTIONS; ++i) {
2230
			if (connections[i] && connections[i]->type == 1 &&
2231
			    !(connections[i]->flags & 2)) {
2232
				freeConnection(i);
2233
			}
2234
		}
2235
		sendResponse(15, 0, {});
2236
		return;
2237
	}
2238
	auto* conn = getConnection(num - 1);
2239
	if (!conn || conn->type != 1) {
2240
		sendResponse(15, 11); // ERR_NO_CONN
2241
		return;
2242
	}
2243
	conn->state = 3;
2244
	freeConnection(num - 1);
2245
	sendResponse(15, 0, {});
2246
}
2247
2248
void EmulatedEsp32::cmdTcpState(std::span<const uint8_t> data)
2249
{
2250
	if (data.size() != 1) {
2251
		sendResponse(16, 4); // ERR_INV_PARAM
2252
		return;
2253
	}
2254
	int num = data[0];
2255
	if (num == 0 || num > MAX_CONNECTIONS) {
2256
		sendResponse(16, 11); // ERR_NO_CONN
2257
		return;
2258
	}
2259
	std::scoped_lock lock(connectionsMutex);
2260
	const auto* conn = getConnection(num - 1);
2261
	if (!conn || conn->type != 1) {
2262
		sendResponse(16, 11); // ERR_NO_CONN
2263
		return;
2264
	}
2265
2266
	// Connection failed (e.g. TLS handshake or certificate error): report
2267
	// ERR_NO_CONN with the close reason stored when it failed (spec 4.5.4)
2268
	if (conn->state == 4) {
2269
		uint8_t reason = closeReason[num - 1].load();
2270
		sendResponse(16, 11, {&reason, 1}); // ERR_NO_CONN
2271
		return;
2272
	}
2273
2274
	// Passive connection with no client attached yet: LISTEN state, only
2275
	// the local port is meaningful (like the firmware's TcpState)
2276
	if (conn->listenSock != OPENMSX_INVALID_SOCKET &&
2277
	    conn->clientSock == OPENMSX_INVALID_SOCKET) {
2278
		std::array<uint8_t, 16> resp{};
2279
		resp[1] = 1; // LISTEN
2280
		Endian::write_UA_L16(resp.data() + 14, conn->localPort);
2281
		sendResponse(16, 0, resp);
2282
		return;
2283
	}
2284
	// Deliberately not narrow<>(): recvBuffer is unbounded (the reader
2285
	// thread keeps appending until the MSX drains it), so with a slow
2286
	// or stopped MSX it can exceed 16-bit range; the field is 16-bit on
2287
	// the wire, so it wraps like the firmware's bounded-buffer avail
2288
	// would. Capping recvBuffer would make narrow<>() safe here.
2289
	auto avail = static_cast<uint16_t>(conn->recvAvail());
2290
2291
	// Info block, same layout as the firmware's TcpState():
2292
	// TLS flag (1) + lwIP state (1) + available (2 LSB MSB)
2293
	// + urgent (2) + send space (2 LSB MSB, capped at 2048)
2294
	// + remote IP (4) + remote port (2 LSB MSB) + local port (2 LSB MSB)
2295
	std::array<uint8_t, 16> resp{};
2296
	resp[0] = conn->ssl ? 1 : 0;  // TLS flag (spec 4.5.5, bit 0)
2297
	// Report the live connection state as the firmware does (raw lwIP
2298
	// TCP state): the reader thread sets state=3 when the remote side
2299
	// closes the connection, which maps to CLOSE_WAIT (7). Per the spec's
2300
	// robustness recommendation, keep reporting ESTABLISHED while there
2301
	// is still unconsumed incoming data.
2302
	uint8_t tcpState = 4;              // ESTABLISHED
2303
	if (conn->state == 1) {
2304
		tcpState = 2;              // opening -> SYN_SENT
2305
	} else if (conn->state == 3) {
2306
		tcpState = (avail > 0) ? 4 : 7; // CLOSE_WAIT (7), ESTABLISHED (4) if data pending
2307
	}
2308
	resp[1] = tcpState;
2309
	Endian::write_UA_L16(resp.data() + 2, avail);
2310
	resp[4] = 0;                       // no urgent data
2311
	resp[5] = 0;
2312
	uint16_t sendSpace = 2048;         // arbitrary, capped like the firmware
2313
	Endian::write_UA_L16(resp.data() + 6, sendSpace);
2314
	Endian::write_UA_B32(resp.data() + 8, conn->remoteIP);
2315
	Endian::write_UA_L16(resp.data() + 12, conn->remotePort);
2316
	Endian::write_UA_L16(resp.data() + 14, conn->localPort);
2317
2318
	sendResponse(16, 0, resp);
2319
}
2320
2321
void EmulatedEsp32::cmdTcpSend(std::span<const uint8_t> data)
2322
{
2323
	// conn num (1) + flags (1) + payload. Per spec: bit 0 = PUSH, bit 1 =
2324
	// urgent (ignored: urgent data not supported); bits 2-7 are unused.
2325
	if (data.size() < 3) {
2326
		sendResponse(17, 11); // ERR_NO_CONN (cannot parse conn number)
2327
		return;
2328
	}
2329
	if (data[1] & 0xfc) {
2330
		sendResponse(17, 4); // ERR_INV_PARAM (unused flag set)
2331
		return;
2332
	}
2333
	int num = data[0];
2334
	if (num == 0 || num > MAX_CONNECTIONS) {
2335
		sendResponse(17, 11); // ERR_NO_CONN
2336
		return;
2337
	}
2338
	uint8_t sendFlags = data[1];
2339
	(void)sendFlags;
2340
2341
	std::scoped_lock lock(connectionsMutex);
2342
	auto* conn = getConnection(num - 1);
2343
	if (!conn || conn->type != 1) {
2344
		sendResponse(17, 11); // ERR_NO_CONN
2345
		return;
2346
	}
2347
	if (conn->listenSock != OPENMSX_INVALID_SOCKET &&
2348
	    conn->clientSock == OPENMSX_INVALID_SOCKET) {
2349
		// Connection is in LISTEN state (spec: ERR_CONN_STATE)
2350
		sendResponse(17, 12); // ERR_CONN_STATE
2351
		return;
2352
	}
2353
	// Passive connections send through the accepted client socket; active
2354
	// ones through the connected socket.
2355
	SOCKET sendSock = (conn->listenSock != OPENMSX_INVALID_SOCKET)
2356
	                ? conn->clientSock : conn->sock;
2357
2358
	auto payload = data.subspan(2);
2359
	ptrdiff_t n;
2360
	if (conn->ssl) {
2361
		// TLS: sending is only possible once the handshake has finished;
2362
		// before that the connection is still in SYN-SENT (spec 4.5.2:
2363
		// data can only be sent on ESTABLISHED/CLOSE_WAIT connections)
2364
		if (conn->handshakePhase.load() != 2) {
2365
			sendResponse(17, 12); // ERR_CONN_STATE
2366
			return;
2367
		}
2368
		auto r = conn->ssl->write(payload);
2369
		if (!r) {
2370
			auto& err = r.error();
2371
			if (err) { // WouldBlock or Closed: no real error
2372
				if (*err == OpenSSL::IoResult::Closed) {
2373
					// Peer sent close_notify: the connection is closed
2374
					conn->state = 3; // remote closed -> CLOSE_WAIT
2375
					sendResponse(17, 12); // ERR_CONN_STATE
2376
				} else {
2377
					// WouldBlock: send buffer full. Report the retryable
2378
					// ERR_BUFFER, like the plain-socket path does. Note:
2379
					// SSL_write may have partially written a record before
2380
					// blocking, so a driver retrying the same payload can
2381
					// in rare cases deliver it twice.
2382
					sendResponse(17, 13); // ERR_BUFFER
2383
				}
2384
			} else {
2385
				// Real TLS error: report ERR_CONN_STATE and log the code
2386
				logTlsError(err.error());
2387
				sendResponse(17, 12); // ERR_CONN_STATE
2388
			}
2389
			return;
2390
		}
2391
		n = static_cast<ptrdiff_t>(*r);
2392
	} else {
2393
		n = sockSend(sendSock, payload);
2394
	}
2395
	if (n < 0) {
2396
		sendResponse(17, 12); // ERR_CONN_STATE
2397
		return;
2398
	}
2399
	if (static_cast<size_t>(n) != payload.size()) {
2400
		sendResponse(17, 13); // ERR_BUFFER
2401
		return;
2402
	}
2403
	sendResponse(17, 0, {});
2404
}
2405
2406
void EmulatedEsp32::cmdTcpRcv(std::span<const uint8_t> data)
2407
{
2408
	if (data.size() != 3) {
2409
		sendResponse(18, 4); // ERR_INV_PARAM
2410
		return;
2411
	}
2412
	int num = data[0];
2413
	if (num == 0 || num > MAX_CONNECTIONS) {
2414
		sendResponse(18, 11); // ERR_NO_CONN
2415
		return;
2416
	}
2417
	uint16_t maxSize = Endian::read_UA_L16(data.data() + 1);
2418
	if (maxSize > 2048) maxSize = 2048;
2419
2420
	std::scoped_lock lock(connectionsMutex);
2421
	auto* conn = getConnection(num - 1);
2422
	if (!conn || conn->type != 1) {
2423
		sendResponse(18, 11); // ERR_NO_CONN
2424
		return;
2425
	}
2426
	if (conn->listenSock != OPENMSX_INVALID_SOCKET &&
2427
	    conn->clientSock == OPENMSX_INVALID_SOCKET) {
2428
		// Connection is in LISTEN state: no data can be retrieved, but
2429
		// per spec 4.5.6 RCV can be called regardless of the state and
2430
		// simply returns zero bytes with ERR_OK
2431
		sendResponse(18, 0, {});
2432
		return;
2433
	}
2434
2435
	std::vector<uint8_t> buf;
2436
	conn->popRecv(buf, maxSize);
2437
2438
	if (buf.empty()) {
2439
		// Firmware: OK response with size 0 (no urgent pointer bytes)
2440
		sendResponse(18, 0, {});
2441
		return;
2442
	}
2443
2444
	// Response: urgent(2 LSB MSB) + data
2445
	std::vector<uint8_t> resp;
2446
	resp.reserve(2 + buf.size());
2447
	resp.push_back(0); // urgent LSB
2448
	resp.push_back(0); // urgent MSB (no urgent data)
2449
	resp.insert(resp.end(), buf.begin(), buf.end());
2450
2451
	sendResponse(18, 0, resp);
2452
}
2453
2454
void EmulatedEsp32::cmdCfgAutoIP(std::span<const uint8_t> data)
2455
{
2456
	// action (1) + config (1, bits: IP automatic / DNS automatic)
2457
	if (data.size() != 2 || data[0] > 1 || data[1] > 3) {
2458
		sendResponse(25, 4); // ERR_INV_PARAM
2459
		return;
2460
	}
2461
	uint8_t action = data[0];
2462
	uint8_t config = data[1];
2463
	uint8_t result = config; // just return the requested value
2464
	if (action == 0) { // GET
2465
		result = 3; // DHCP for IP + DNS
2466
	}
2467
	sendResponse(25, 0, {&result, 1});
2468
}
2469
2470
void EmulatedEsp32::cmdCfgIP(std::span<const uint8_t> data)
2471
{
2472
	// field (1, 1..6 except 2) + IP (4)
2473
	if (data.size() != 5 || data[0] == 0 || data[0] == 2 || data[0] > 6) {
2474
		sendResponse(26, 4); // ERR_INV_PARAM
2475
		return;
2476
	}
2477
	// Just store in settings (optional) and return OK
2478
	sendResponse(26, 0, {});
2479
}
2480
2481
} // namespace openmsx