OXYGENHUB
◆ main openmsx / src / network / UnapiNet.cc
56.1 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
e8555c89 Andy McCall 1
#include "UnapiNet.hh"
2
3
#include "MSXException.hh"
4
#include "one_of.hh"
5
#include "serialize.hh"
6
7
#ifdef _WIN32
8
#include <ws2tcpip.h>
9
#include <iphlpapi.h>
10
#include <icmpapi.h>
11
#ifdef interface
12
// The Microsoft headers above define the 'interface' macro again (Socket.hh
13
// already undoes the one from winsock2). Undo it here too, so it cannot
14
// clobber openMSX code that uses the word as an identifier.
15
#undef interface
16
#endif
17
#else
18
#include <arpa/inet.h>
19
#include <netdb.h>
20
#include <netinet/tcp.h>
21
#include <errno.h>
22
#endif
23
24
#include <algorithm>
25
#include <cassert>
26
#include <chrono>
27
#include <cstring>
28
#include <expected>
29
30
// Socket handles are openMSX SOCKET values (see Socket.hh); no casts needed.
31
32
// UnapiNet - MSX-UNAPI TCP/IP bridge device
33
//
34
// Bridge between the MSX I/O ports and BSD sockets on the host, speaking
35
// protocol v2 (see network/protocol-v2.md): every reply begins with a
36
// status byte carrying an MSX-UNAPI error code verbatim, so the Z80 driver
37
// hands it to its caller without translating. Async DNS, TCP and UDP (4
38
// connections each), ICMP echo where the host supports it.
39
40
namespace openmsx {
41
42
// MSX-UNAPI TCP/IP error codes: v2 speaks them natively on the wire (the
43
// status byte fronting every reply), so these values are shared with the
44
// Z80 driver's error table.
45
static constexpr uint8_t ERR_OK           = 0;
46
static constexpr uint8_t ERR_NOT_IMP      = 1;
47
static constexpr uint8_t ERR_NO_NETWORK   = 2;
48
static constexpr uint8_t ERR_NO_DATA      = 3;
49
static constexpr uint8_t ERR_INV_PARAM    = 4;
50
static constexpr uint8_t ERR_QUERY_EXISTS = 5;
51
static constexpr uint8_t ERR_NO_FREE_CONN = 9;
52
static constexpr uint8_t ERR_NO_CONN      = 11;
53
static constexpr uint8_t ERR_CONN_STATE   = 12;
54
static constexpr uint8_t ERR_BUFFER       = 13;
55
56
// DETECT capability byte: bits 0-3 are informational and always set on this
57
// device; bit4 (ICMP) is honest - latched once at device start and enforced
58
// by the ICMP commands.
59
static constexpr uint8_t CAPS_BASE = 0x0F; // DNS + TCP active/passive + UDP
60
static constexpr uint8_t CAP_ICMP  = 0x10;
61
62
// Maximum result payload per TCP_RECV command, and the exact-size ceiling
63
// on a TCP_SEND length (rule 3 makes exceeding it ERR_INV_PARAM).
64
static constexpr size_t MAX_TRANSFER = 4096;
65
66
// Maximum receive buffer size per connection
67
// BBS ANSI screens can be 16-32KB, needs big buffer
68
static constexpr size_t MAX_RECV_BUF = 65536;
69
70
// Upper bound on accumulated command parameters: one byte MORE than the
71
// largest legal parameter block (UDP_SEND: 9-byte header + a full 16-bit
72
// payload). A block the cap truncated therefore always ends on an illegal
73
// size, so every command's size check rejects it - no separate overflow
74
// flag needed - while a runaway MSX program hammering the data port still
75
// cannot exhaust host memory.
76
static constexpr size_t MAX_PARAM_BUF = sizeof(UdpSendParamHeader) + 0xFFFF + 1;
77
78
// DNS_QUERY's own size cap (rule 3 "too long"): the DNS presentation-form
79
// name limit (RFC 1035). Keeps the one variable-length command that has no
80
// declared payload length from depending on MAX_PARAM_BUF for rejection.
81
static constexpr size_t MAX_HOSTNAME = 253;
82
83
// Bound on data queued for sending but not yet accepted by the kernel.
84
// TCP_SEND reports ERR_BUFFER rather than blocking the emulation thread.
85
static constexpr size_t MAX_SEND_BUF = 64 * 1024;
86
87
// Bounds on the receive-side queues (v1 values, unchanged in v2).
88
static constexpr size_t MAX_UDP_QUEUE  = 16;   // pending datagrams per socket
89
static constexpr size_t MAX_ICMP_QUEUE = 16;   // pending echo replies
90
static constexpr size_t MAX_UDP_DGRAM  = 2048; // larger datagrams are truncated at receive
91
92
// Bridge command opcodes (wire protocol, shared with the Z80 driver).
93
// 0x10 (v1's QUERY_CAP) is retired: DETECT carries the capabilities, and
94
// the opcode answers ERR_NOT_IMP like any other unknown one.
95
static constexpr uint8_t CMD_DETECT      = 0x00;
96
static constexpr uint8_t CMD_DNS_QUERY   = 0x01;
97
static constexpr uint8_t CMD_DNS_STATUS  = 0x02;
98
static constexpr uint8_t CMD_TCP_OPEN    = 0x03;
99
static constexpr uint8_t CMD_TCP_SEND    = 0x04;
100
static constexpr uint8_t CMD_TCP_RECV    = 0x05;
101
static constexpr uint8_t CMD_TCP_CLOSE   = 0x06;
102
static constexpr uint8_t CMD_TCP_STATE   = 0x07;
103
static constexpr uint8_t CMD_TCP_ABORT   = 0x08;
104
static constexpr uint8_t CMD_UDP_OPEN    = 0x09;
105
static constexpr uint8_t CMD_UDP_CLOSE   = 0x0A;
106
static constexpr uint8_t CMD_UDP_STATE   = 0x0B;
107
static constexpr uint8_t CMD_UDP_SEND    = 0x0C;
108
static constexpr uint8_t CMD_GET_LOCALIP = 0x0D;
109
static constexpr uint8_t CMD_NET_STATE   = 0x0E;
110
static constexpr uint8_t CMD_UDP_RECV    = 0x0F;
111
static constexpr uint8_t CMD_ICMP_SEND   = 0x11;
112
static constexpr uint8_t CMD_ICMP_RECV   = 0x12;
113
114
// How long a half-closed connection may wait for the peer to close too,
115
// before we drop it and free the handle.
116
static constexpr auto CLOSE_TIMEOUT = std::chrono::seconds(30);
117
118
// openMSX's sock_recv()/sock_send() fold 'peer closed' and 'error' into -1,
119
// and decide would-block from SO_ERROR - which a synchronous WSAEWOULDBLOCK
120
// does not set on Windows. We need the outcomes apart, so we call
121
// recv()/send() here and ask the platform ourselves.
122
enum class IoError { WouldBlock, Closed, Failed };
123
using IoResult = std::expected<size_t, IoError>; // success: bytes transferred
124
125
// 'try again later' rather than a broken connection.
126
[[nodiscard]] static bool ioWouldBlock()
127
{
128
#ifdef _WIN32
129
	int err = WSAGetLastError();
130
	return (err == WSAEWOULDBLOCK) || (err == WSAEINTR);
131
#else
132
	return (errno == EWOULDBLOCK) || (errno == EAGAIN) || (errno == EINTR);
133
#endif
134
}
135
136
[[nodiscard]] static IoResult netRecv(SOCKET sd, char* buf, size_t count)
137
{
138
	auto n = recv(sd, buf, static_cast<int>(count), 0);
139
	if (n > 0) return static_cast<size_t>(n);
140
	if (n == 0) return std::unexpected(IoError::Closed); // orderly shutdown by the peer
141
	return std::unexpected(ioWouldBlock() ? IoError::WouldBlock : IoError::Failed);
142
}
143
144
[[nodiscard]] static IoResult netSend(SOCKET sd, const uint8_t* buf, size_t count)
145
{
146
	// MSG_NOSIGNAL: writing to a connection the peer has reset raises SIGPIPE
147
	// otherwise, and nothing in openMSX ignores that signal - it would take
148
	// the whole emulator down.
149
#ifdef MSG_NOSIGNAL
150
	constexpr int SEND_FLAGS = MSG_NOSIGNAL;
151
#else
152
	constexpr int SEND_FLAGS = 0; // Windows has no SIGPIPE
153
#endif
154
	auto n = send(sd, reinterpret_cast<const char*>(buf),
155
	              static_cast<int>(count), SEND_FLAGS);
156
	if (n >= 0) return static_cast<size_t>(n);
157
	return std::unexpected(ioWouldBlock() ? IoError::WouldBlock : IoError::Failed);
158
}
159
160
// Half-close: tell the peer we are done sending (FIN).
161
static void shutdownSend(SOCKET sd)
162
{
163
#ifdef _WIN32
164
	shutdown(sd, SD_SEND);
165
#else
166
	shutdown(sd, SHUT_WR);
167
#endif
168
}
169
170
// UNAPI DNS_S sub-error reported on a failed lookup (host name does not exist)
171
static constexpr uint8_t DNS_ERR_NO_SUCH_HOST = 3;
172
173
// Constructor / Destructor
174
175
UnapiNet::UnapiNet(const DeviceConfig& config)
176
	: MSXDevice(config)
177
{
178
	paramBuf.reserve(MAX_TRANSFER + 16);
179
	resultBuf.reserve(MAX_TRANSFER + 16);
180
181
	reset(EmuTime::dummy()); // keep constructor and reset() in sync
182
183
	// The loopback wake pair (see the header). Two UDP sockets on
184
	// 127.0.0.1: if any step fails, the host's network stack is broken
185
	// and every socket this device would later hand out is doomed the
186
	// same way - so refuse to construct instead of running degraded.
187
	// Requiring the pair makes poke() a guaranteed wake-up and leaves
188
	// the socket loop a single code path, the one that gets tested.
189
	// (Created before the ICMP handle: if this throws, the destructor
190
	// never runs, so nothing needing cleanup may precede it.)
191
	SOCKET r = socket(AF_INET, SOCK_DGRAM, 0);
192
	if (r == OPENMSX_INVALID_SOCKET) {
193
		throw MSXException("UnapiNet: cannot create the wake socket pair");
194
	}
195
	sockaddr_in addr = sock_makeIPv4(INADDR_LOOPBACK, 0); // any free port
196
	::socklen_t alen = sizeof(addr);
197
	if (bind(r, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) != 0 ||
198
	    getsockname(r, reinterpret_cast<sockaddr*>(&addr), &alen) != 0) {
199
		sock_close(r);
200
		throw MSXException("UnapiNet: cannot bind the wake socket pair");
201
	}
202
	SOCKET s = socket(AF_INET, SOCK_DGRAM, 0);
203
	if (s == OPENMSX_INVALID_SOCKET ||
204
	    connect(s, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) != 0) {
205
		if (s != OPENMSX_INVALID_SOCKET) sock_close(s);
206
		sock_close(r);
207
		throw MSXException("UnapiNet: cannot connect the wake socket pair");
208
	}
209
	sock_setNonBlocking(r);
210
	sock_setNonBlocking(s);
211
	wakeRecv = r;
212
	wakeSend = s;
213
214
#ifdef _WIN32
215
	// Latch the ICMP capability, once: DETECT advertises bit4 only when
216
	// pinging can actually work, and the ICMP commands are gated on it.
217
	// (IcmpSendEcho needs no privileges, unlike raw sockets.)
218
	if (HANDLE h = IcmpCreateFile(); h != INVALID_HANDLE_VALUE) {
219
		icmpChannel = h;
220
		icmpAvailable = true;
221
	}
222
#endif
223
224
	// Start background threads
225
	running = true;
226
	sockThread = std::thread([this]() { socketLoop(); });
227
	dnsThread  = std::thread([this]() { dnsWorkerLoop(); });
228
	if (icmpAvailable) {
229
		icmpWorker = std::thread([this]() { icmpWorkerLoop(); });
230
	}
231
}
232
233
UnapiNet::~UnapiNet()
234
{
235
	running = false;
236
	poke(); // leave select() now instead of at the next timeout
237
	if (sockThread.joinable()) sockThread.join();
238
	// Wake the ICMP worker the same way as the DNS worker below (the
239
	// empty critical section is explained there). If it is inside
240
	// IcmpSendEcho() the join waits the call out (capped at 2 s).
241
	{
242
		std::scoped_lock lock(icmpMutex);
243
	}
244
	icmpCv.notify_all();
245
	if (icmpWorker.joinable()) icmpWorker.join();
246
	// Wake the DNS worker so it observes the shutdown. If it is inside
247
	// getaddrinfo() the join waits that call out - deliberate, so the
248
	// resolver cannot write into a destroyed object.
249
	//
250
	// The empty critical section is load-bearing, not a refactoring
251
	// leftover: it closes the lost-wakeup window. The worker evaluates its
252
	// wait predicate under this mutex and cv.wait() releases-and-parks
253
	// atomically, so taking the mutex once *after* flipping 'running'
254
	// guarantees the worker is either before the predicate (it will see
255
	// running == false) or already parked (it will get the notify below).
256
	// Without it, the notify could fire in the gap between the worker
257
	// checking 'running' and parking - and the join would wait forever.
258
	{
259
		std::scoped_lock lock(dns.mutex);
260
	}
261
	dns.cv.notify_all();
262
	if (dnsThread.joinable())  dnsThread.join();
263
	closeAllConnections();
264
	// The socket thread stopped without draining its queue: close what is left,
265
	// or we leak every fd handed over in the last select cycle.
266
	{
267
		std::scoped_lock lock(closeMutex);
268
		for (SOCKET sd : socksToClose) sock_close(sd);
269
		socksToClose.clear();
270
	}
271
	sock_close(wakeSend);
272
	sock_close(wakeRecv);
273
274
#ifdef _WIN32
275
	if (icmpChannel) IcmpCloseHandle(icmpChannel);
276
#endif
277
}
278
279
// Reset
280
281
void UnapiNet::reset(EmuTime /*time*/)
282
{
283
	// Ground state: every connection closed and freed, every buffer and
284
	// queue discarded, pending reply and accumulated parameters dropped,
285
	// and DNS idle. The ICMP capability stays as latched at device start.
286
	paramBuf.clear();
287
	resultBuf.clear();
288
	resultPos = 0;
289
290
	// Ask the socket thread to drop everything (it owns the sock_close()).
291
	// During construction the thread doesn't exist yet - but neither do any
292
	// sockets, so this is a no-op then.
293
	for (auto& c : tcp) requestClose(c, CloseReason::NeverUsed, true);
294
	for (auto& u : udp) requestClose(u);
295
296
	{
297
		std::scoped_lock lock(dns.mutex);
298
		dns.status = DnsStatus::Idle;
299
		dns.resolvedIp = 0;
300
		dns.request.reset(); // disowns a lookup still in flight (it only
301
		                     // publishes while 'request' holds its name)
302
		                     // and discards a queued one in the same move
303
	}
304
	{
305
		std::scoped_lock lock(icmpMutex);
306
		icmpReplies.clear();
307
		++icmpGeneration; // an echo still in flight must not repopulate
308
		                  // the queue we just cleared
309
		icmpPending = false; // and a queued-but-unstarted one never starts
310
	}
311
}
312
313
// Port reads
314
315
byte UnapiNet::peekIO(uint16_t /*port*/, EmuTime /*time*/) const
316
{
317
	// Only the data register (typically 0x29) is registered for reads; the
318
	// command port is write-only in v2, so a read there decodes to the
319
	// open bus without reaching us. This returns the next unread reply
320
	// byte. Past the end of a reply, or with no reply pending, reads 0xFF -
321
	// the open-bus value an absent device yields, and never a valid
322
	// status (rule 4): a desynchronized driver can always tell.
323
	if (resultPos < resultBuf.size()) {
324
		return resultBuf[resultPos];
325
	}
326
	return 0xFF;
327
}
328
329
byte UnapiNet::readIO(uint16_t port, EmuTime time)
330
{
331
	byte b = peekIO(port, time);
332
	// reading the data register consumes one result byte
333
	if (resultPos < resultBuf.size()) {
334
		++resultPos;
335
	}
336
	return b;
337
}
338
339
// Port writes
340
341
void UnapiNet::writeIO(uint16_t port, byte value, EmuTime /*time*/)
342
{
343
	if (port & 1) {
344
		// parameter byte (typically 0x29). Writing one while a reply is
345
		// pending abandons the reply (the recovery rule) - the driver never
346
		// has to drain a result it lost interest in.
347
		resultBuf.clear();
348
		resultPos = 0;
349
		if (paramBuf.size() < MAX_PARAM_BUF) {
350
			paramBuf.push_back(value);
351
		}
352
		// else: drop the byte. The cap is one past the largest legal
353
		// block, so a truncated block is already "too long" (rule 3) at
354
		// every command's size check - dropping loses nothing.
355
	} else {
356
		// command (typically 0x28); replaces a pending reply, if any
357
		processCmd(value);
358
	}
359
}
360
361
// Result helpers
362
363
void UnapiNet::setResult(std::span<const uint8_t> data)
364
{
365
	assert(!data.empty()); // v2: every reply begins with its status byte
366
	resultBuf.assign(data.begin(), data.end());
367
	resultPos = 0;
368
}
369
370
void UnapiNet::replyStatus(uint8_t status)
371
{
372
	setResult(std::span<const uint8_t>(&status, 1));
373
}
374
375
// TCP handle management
376
377
int UnapiNet::allocTcpHandle()
378
{
379
	// A closed slot holding the sticky tail of a dead connection IS free:
380
	// it does not count toward ERR_NO_FREE_CONN, and reuse destroys the tail
381
	// (cmdTcpOpen clears the buffers when it publishes).
382
	for (int i = 0; i < MAX_TCP; i++) {
383
		if (tcp[i].sock == OPENMSX_INVALID_SOCKET &&
384
			tcp[i].tcpState == TcpState::Closed) {
385
			return i;
386
		}
387
	}
388
	return INVALID_HANDLE;
389
}
390
391
// The wire handle is a byte from the MSX: 1-based, 1..4 in range. These two
392
// are the only places that map wire handles to the 0-based internal arrays.
393
UnapiNet::TcpConnection* UnapiNet::tcpForHandle(int wireHandle)
394
{
395
	return (wireHandle >= 1 && wireHandle <= MAX_TCP) ? &tcp[wireHandle - 1] : nullptr;
396
}
397
398
UnapiNet::UdpConnection* UnapiNet::udpForHandle(int wireHandle)
399
{
400
	return (wireHandle >= 1 && wireHandle <= MAX_UDP) ? &udp[wireHandle - 1] : nullptr;
401
}
402
403
// Direct close. Only safe with the socket thread stopped (destructor).
404
void UnapiNet::closeTcp(TcpConnection& c)
405
{
406
	std::scoped_lock lock(c.mutex);
407
	if (SOCKET sd = c.sock; sd != OPENMSX_INVALID_SOCKET) {
408
		sock_close(sd);
409
		c.sock = OPENMSX_INVALID_SOCKET;
410
	}
411
	c.finSent = false;
412
	c.tcpState   = TcpState::Closed;
413
	c.remoteIp   = 0;
414
	c.remotePort = 0;
415
	c.localPort  = 0;
416
	c.resident   = false;
417
	c.recvBuf.clear();
418
	c.sendBuf.clear();
419
}
420
421
void UnapiNet::closeAllConnections()
422
{
423
	for (auto& c : tcp) {
424
		closeTcp(c);
425
		c.closeReason = CloseReason::NeverUsed;
426
	}
427
	for (auto& u : udp) {
428
		closeUdp(u);
429
	}
430
}
431
432
// Network socket thread (background)
433
//
434
// One select() loop serves every socket: it moves incoming TCP data into
435
// each connection's recvBuf, flushes queued sends, detects completion of
436
// a non-blocking connect() and state transitions (remote close, etc.),
437
// receives UDP datagrams, and performs every close. It sleeps in
438
// select() until traffic arrives or the emulation thread pokes the wake
439
// socket at it.
440
441
// Wake the socket thread out of select(): one byte to the loopback
442
// socket it always watches (the constructor guarantees the pair
443
// exists). The result is deliberately dropped: a refused send can only
444
// mean the wake socket already holds unread pokes, so select() has a
445
// byte to wake on either way - the poke is a hint, never a message,
446
// and the loop re-derives all its work from shared state.
447
void UnapiNet::poke()
448
{
449
	uint8_t b = 0;
450
	[[maybe_unused]] auto result = netSend(wakeSend, &b, 1);
451
}
452
453
void UnapiNet::deferSockClose(SOCKET sd)
454
{
455
	std::scoped_lock lock(closeMutex);
456
	socksToClose.push_back(sd);
457
	poke(); // the fd is closed at the top of the loop's next pass
458
}
459
460
void UnapiNet::requestClose(TcpConnection& c, CloseReason reason,
461
                            bool clearMetadata)
462
{
463
	SOCKET sd;
464
	{
465
		std::scoped_lock lock(c.mutex);
466
		sd = c.sock;
467
		c.sock = OPENMSX_INVALID_SOCKET; // the handle is reusable right away
468
		c.closeReason = reason;
469
		c.tcpState = TcpState::Closed;
470
		c.finSent = false;
471
		c.sendBuf.clear();
472
		if (clearMetadata) { // reset() only: the sticky-slot rule keeps
473
			c.remoteIp   = 0;  // the endpoint info and the undrained tail
474
			c.remotePort = 0;  // readable everywhere else
475
			c.localPort  = 0;
476
			c.resident   = false;
477
			c.recvBuf.clear();
478
		}
479
	}
480
	if (sd != OPENMSX_INVALID_SOCKET) deferSockClose(sd);
481
}
482
483
void UnapiNet::requestClose(UdpConnection& u)
484
{
485
	// Unlike TCP there is no sticky tail: closing a UDP socket discards
486
	// its queued datagrams.
487
	SOCKET sd;
488
	{
489
		std::scoped_lock lock(u.mutex);
490
		sd = u.sock;
491
		u.sock = OPENMSX_INVALID_SOCKET;
492
		u.localPort = 0;
493
		u.resident  = false;
494
		u.recvQueue.clear();
495
	}
496
	if (sd != OPENMSX_INVALID_SOCKET) deferSockClose(sd);
497
}
498
499
// TCP_CLOSE. A listening or still-connecting socket has nothing to
500
// half-close, so it is simply dropped. An established one moves to
501
// FinWait1: the FIN goes out as soon as whatever the MSX queued has been
502
// sent, and the connection is dropped when the peer closes too (or when
503
// CLOSE_TIMEOUT passes and it never does). A second CLOSE while that is
504
// in progress changes nothing (idempotent).
505
void UnapiNet::gracefulClose(TcpConnection& c)
506
{
507
	{
508
		// Test and act under one lock: the socket thread may have dropped this
509
		// connection (peer reset) between the two, which would leave FinWait1
510
		// stamped on a slot that has no socket - unusable and unrecoverable.
511
		std::scoped_lock lock(c.mutex);
512
		SOCKET sd = c.sock;
513
		if (sd != OPENMSX_INVALID_SOCKET) {
514
			if (c.tcpState == TcpState::FinWait1) {
515
				return; // close already in progress: idempotent
516
			}
517
			if (c.tcpState == one_of(TcpState::Established, TcpState::CloseWait)) {
518
				c.closeReason = CloseReason::ClosedByUser;
519
				c.tcpState = TcpState::FinWait1;
520
				c.closeDeadline = std::chrono::steady_clock::now() + CLOSE_TIMEOUT;
521
				c.finSent = false;
522
				if (c.sendBuf.empty()) {
523
					shutdownSend(sd);
524
					c.finSent = true;
525
				}
526
				poke(); // the loop now tracks this close (FIN flush,
527
				        // peer's close, deadline)
528
				return;
529
			}
530
		}
531
	} // drop the lock: requestClose() takes it
532
	// Nothing to half-close (listening, still connecting, or already gone).
533
	requestClose(c, CloseReason::ClosedByUser);
534
}
535
536
void UnapiNet::socketLoop()
537
{
538
	while (running) {
539
		// Close whatever the emulation thread handed over. Doing it here, at the
540
		// top of a pass, means no select() is in flight on those fds, so their
541
		// numbers cannot be recycled under a thread that is still watching them.
542
		{
543
			std::vector<SOCKET> toClose;
544
			{
545
				std::scoped_lock lock(closeMutex);
546
				toClose.swap(socksToClose);
547
			}
548
			for (SOCKET sd : toClose) sock_close(sd);
549
		}
550
551
		// Wait on all active sockets at once with a single select(),
552
		// instead of busy-polling each socket in turn. The wake socket is
553
		// always in the read set, so new work from the emulation thread
554
		// interrupts the wait immediately; the timeout is a backstop - it
555
		// paces the FinWait1 deadline check.
556
		fd_set rfds;
557
		fd_set wfds;
558
		fd_set efds;
559
		FD_ZERO(&rfds);
560
		FD_ZERO(&wfds);
561
		FD_ZERO(&efds);
562
		// The wake socket goes into every read set (the constructor
563
		// guarantees the pair exists), so select() always has at least
564
		// one fd to watch - there is no "empty set" case to special-case.
565
		FD_SET(wakeRecv, &rfds);
566
		SOCKET maxSock = wakeRecv;
567
		// Remember exactly which fd we armed for each slot: after select() the
568
		// emulation thread may have closed and reopened one, and a stale bit in
569
		// the fd sets must not be applied to the new socket.
570
		std::array<SOCKET, MAX_TCP> watchedTcp;
571
		std::array<SOCKET, MAX_UDP> watchedUdp;
572
		watchedTcp.fill(OPENMSX_INVALID_SOCKET);
573
		watchedUdp.fill(OPENMSX_INVALID_SOCKET);
574
575
		for (int i = 0; i < MAX_TCP; ++i) {
576
			auto& c = tcp[i];
577
			SOCKET sd = c.sock;
578
			if (sd == OPENMSX_INVALID_SOCKET) continue;
579
580
			// Half-close in progress (the MSX called TCP_CLOSE).
581
			if (c.tcpState == TcpState::FinWait1) {
582
				bool giveUp = false;
583
				{
584
					std::scoped_lock lock(c.mutex);
585
					if (!c.finSent && c.sendBuf.empty()) {
586
						// Everything the MSX queued is out: now it is safe to FIN.
587
						shutdownSend(sd);
588
						c.finSent = true;
589
					}
590
					giveUp = std::chrono::steady_clock::now() > c.closeDeadline;
591
				}
592
				if (giveUp) { // the peer never closed its side
593
					requestClose(c, CloseReason::ClosedByUser);
594
					continue;
595
				}
596
			}
597
598
			watchedTcp[i] = sd;
599
			if (c.tcpState == TcpState::SynSent) {
600
				// connect() completion shows up as writable (POSIX) or in
601
				// the exception set (Windows).
602
				FD_SET(sd, &wfds);
603
				FD_SET(sd, &efds);
604
			} else {
605
				// Only ask for incoming data while recvBuf has room for it.
606
				// Leaving a full socket unread lets the kernel receive buffer
607
				// fill up, which closes the TCP window and makes the peer stop
608
				// sending - that is the flow control TCP already provides.
609
				// recv()ing anyway and dropping whatever doesn't fit would
610
				// silently truncate the stream instead.
611
				// While recvBuf is full we don't notice the peer closing its
612
				// side either; that is picked up as soon as the MSX drains
613
				// some bytes and the socket is armed again.
614
				bool pendingSend;
615
				bool hasRoom;
616
				{
617
					std::scoped_lock lock(c.mutex);
618
					pendingSend = !c.sendBuf.empty();
619
					hasRoom = c.recvBuf.size() < MAX_RECV_BUF;
620
				}
621
				if (hasRoom)     FD_SET(sd, &rfds);
622
				if (pendingSend) FD_SET(sd, &wfds);
623
			}
624
			maxSock = std::max(maxSock, sd);
625
		}
626
		for (int i = 0; i < MAX_UDP; ++i) {
627
			auto& u = udp[i];
628
			SOCKET sd = u.sock;
629
			if (sd == OPENMSX_INVALID_SOCKET) continue;
630
			watchedUdp[i] = sd;
631
			FD_SET(sd, &rfds);
632
			maxSock = std::max(maxSock, sd);
633
		}
634
		struct timeval tv = {0, 100000}; // 100 ms
635
		if (select(static_cast<int>(maxSock) + 1, &rfds, &wfds, &efds, &tv) <= 0) {
636
			continue; // timeout or error: re-check running and rebuild the set
637
		}
638
		if (FD_ISSET(wakeRecv, &rfds)) {
639
			// Drain every queued poke. Each one is only a hint to rescan;
640
			// all the actual work is re-derived from shared state below.
641
			std::array<char, 64> pokes;
642
			while (netRecv(wakeRecv, pokes.data(), pokes.size())) {}
643
		}
644
645
		for (int i = 0; i < MAX_TCP; ++i) {
646
			auto& c = tcp[i];
647
			SOCKET sd = watchedTcp[i];
648
			// Skip slots we did not arm, and slots whose socket changed while we
649
			// were in select(): the fd sets refer to the old one.
650
			if (sd == OPENMSX_INVALID_SOCKET || c.sock != sd) continue;
651
652
			// Pending connect() completion.
653
			if (c.tcpState == TcpState::SynSent) {
654
				if (FD_ISSET(sd, &efds)) {
655
					requestClose(c, CloseReason::ConnectFailed);
656
				} else if (FD_ISSET(sd, &wfds)) {
657
					int err = sock_getIntOption(sd, SOL_SOCKET, SO_ERROR);
658
					if (err != 0) {
659
						requestClose(c, CloseReason::ConnectFailed);
660
					} else {
661
						std::scoped_lock lock(c.mutex);
662
						if (c.sock == sd) c.tcpState = TcpState::Established;
663
					}
664
				}
665
				continue;
666
			}
667
668
			// Listening socket: accept the pending connection.
669
			if (c.tcpState == TcpState::Listen) {
670
				if (!FD_ISSET(sd, &rfds)) continue;
671
				struct sockaddr_in peer;
672
				::socklen_t plen = sizeof(peer);
673
				SOCKET a = accept(sd, reinterpret_cast<struct sockaddr*>(&peer), &plen);
674
				if (a == OPENMSX_INVALID_SOCKET) continue;
675
				uint32_t peerIp = ntohl(peer.sin_addr.s_addr);
676
				uint16_t peerPort = ntohs(peer.sin_port);
677
				sock_setNonBlocking(a);
678
				sock_setIntOption(a, IPPROTO_TCP, TCP_NODELAY);
679
680
				bool taken = false;
681
				{
682
					std::scoped_lock lock(c.mutex);
683
					// The MSX may have closed the listener while we were accepting,
684
					// and the remote-IP filter has to be read coherently. A
685
					// non-matching peer is dropped and the listener keeps
686
					// listening; on a match the handle BECOMES the connection.
687
					if (c.sock == sd && c.tcpState == TcpState::Listen &&
688
					    (c.remoteIp == 0 || peerIp == c.remoteIp)) {
689
						// Swap the listening socket for the accepted one, publishing
690
						// the state and the address together.
691
						c.sock       = a;
692
						c.tcpState   = TcpState::Established;
693
						c.remoteIp   = peerIp;
694
						c.remotePort = peerPort;
695
						taken = true;
696
					}
697
				}
698
				if (taken) {
699
					deferSockClose(sd); // the old listener; closed next pass
700
				} else {
701
					sock_close(a);
702
				}
703
				continue;
704
			}
705
706
			if (c.tcpState != one_of(TcpState::Established, TcpState::CloseWait,
707
			                         TcpState::FinWait1)) {
708
				continue;
709
			}
710
711
			// Push out whatever the MSX queued via TCP_SEND. send() on a
712
			// non-blocking socket cannot block, so doing it under the lock is
713
			// fine; a full kernel buffer just means 'try again on the next pass'.
714
			if (FD_ISSET(sd, &wfds)) {
715
				bool sendFailed = false;
716
				{
717
					std::scoped_lock lock(c.mutex);
718
					while (c.sock == sd && !c.sendBuf.empty()) {
719
						auto r = netSend(sd, c.sendBuf.data(), c.sendBuf.size());
720
						if (!r) {
721
							sendFailed = (r.error() == IoError::Failed);
722
							break;
723
						}
724
						if (*r == 0) break;
725
						c.sendBuf.erase(c.sendBuf.begin(),
726
						                c.sendBuf.begin() + *r);
727
					}
728
				}
729
				if (sendFailed) {
730
					requestClose(c, CloseReason::ConnectionReset);
731
					continue;
732
				}
733
			}
734
735
			// Incoming data. Never ask the kernel for more than fits: whatever
736
			// we take out of its buffer and cannot store would be lost, and the
737
			// peer would never know. Reading only 'room' bytes leaves the rest
738
			// in the kernel, where it keeps the TCP window closed until the MSX
739
			// makes space.
740
			if (!FD_ISSET(sd, &rfds)) continue;
741
			size_t room;
742
			{
743
				std::scoped_lock lock(c.mutex);
744
				if (c.sock != sd) continue;
745
				room = MAX_RECV_BUF - std::min(MAX_RECV_BUF, c.recvBuf.size());
746
			}
747
			if (room == 0) continue; // full: we shouldn't even have armed it
748
			std::array<char, 512> buf;
749
			auto r = netRecv(sd, buf.data(), std::min(buf.size(), room));
750
			if (r) {
751
				std::scoped_lock lock(c.mutex);
752
				if (c.sock != sd) continue;
753
				const auto* d = reinterpret_cast<const uint8_t*>(buf.data());
754
				c.recvBuf.insert(c.recvBuf.end(), d, d + *r);
755
			} else if (r.error() == IoError::Closed) {
756
				// The peer closed its side. If we had closed ours too, the
757
				// connection is finished; otherwise the MSX may still send.
758
				if (c.tcpState == TcpState::FinWait1) {
759
					requestClose(c, CloseReason::ClosedByUser);
760
				} else {
761
					std::scoped_lock lock(c.mutex);
762
					if (c.sock == sd) c.tcpState = TcpState::CloseWait;
763
				}
764
			} else if (r.error() == IoError::Failed) {
765
				requestClose(c, CloseReason::ConnectionReset);
766
			}
767
			// WouldBlock: spurious readable, nothing to do
768
		}
769
770
		for (int i = 0; i < MAX_UDP; ++i) {
771
			auto& u = udp[i];
772
			SOCKET sd = watchedUdp[i];
773
			if (sd == OPENMSX_INVALID_SOCKET || u.sock != sd) continue;
774
			if (!FD_ISSET(sd, &rfds)) continue;
775
			std::array<char, MAX_UDP_DGRAM> buf;
776
			struct sockaddr_in src;
777
			::socklen_t slen = sizeof(src);
778
			int n = recvfrom(sd, buf.data(), buf.size(), 0,
779
			                 reinterpret_cast<struct sockaddr*>(&src), &slen);
780
#ifdef _WIN32
781
			// Winsock reports a datagram larger than the buffer as an
782
			// ERROR (WSAEMSGSIZE) after filling the buffer and consuming
783
			// the datagram - that IS the truncate-at-receive the protocol
784
			// wants, not a failure. Treating it as one would silently lose
785
			// the whole datagram. (POSIX recvfrom just returns a full
786
			// buffer, no special case needed.)
787
			if (n < 0 && WSAGetLastError() == WSAEMSGSIZE) {
788
				n = static_cast<int>(buf.size());
789
			}
790
#endif
791
			// recvfrom() == 0 is not EOF here: an empty UDP datagram is a
792
			// real datagram (visible to the receiver, unlike a 0-byte TCP
793
			// send) and queues like any other.
794
			if (n < 0) continue;
795
			UdpDatagram dg;
796
			dg.srcIp = ntohl(src.sin_addr.s_addr);
797
			dg.srcPort = ntohs(src.sin_port);
798
			dg.data.assign(buf.data(), buf.data() + n);
799
			std::scoped_lock lock(u.mutex);
800
			if (u.sock != sd) continue;
801
			if (u.recvQueue.size() < MAX_UDP_QUEUE) { // cap pending datagrams
802
				u.recvQueue.push_back(std::move(dg));
803
			}
804
		}
805
	}
806
}
807
808
// Command processing
809
//
810
// Validation order is fixed by the spec: form first (rule 3 plus the
811
// semantic ERR_INV_PARAMs), then capability, then handle, then connection
812
// state, then the data check; the host operation runs last, so its errors
813
// imply every earlier check passed.
814
815
void UnapiNet::processCmd(uint8_t cmd)
816
{
817
	// Opcode dispatch precedes rule 3: an unknown opcode answers ERR_NOT_IMP
818
	// whether or not stray parameter bytes accompany it (there is no
819
	// expected size to validate against).
820
	switch (cmd) {
821
	case CMD_DETECT:     cmdDetect();    break;
822
	case CMD_DNS_QUERY:  cmdDnsQuery();  break;
823
	case CMD_DNS_STATUS: cmdDnsStatus(); break;
824
	case CMD_TCP_OPEN:   cmdTcpOpen();   break;
825
	case CMD_TCP_SEND:   cmdTcpSend();   break;
826
	case CMD_TCP_RECV:   cmdTcpRecv();   break;
827
	case CMD_TCP_CLOSE:  cmdTcpClose();  break;
828
	case CMD_TCP_STATE:  cmdTcpState();  break;
829
	case CMD_TCP_ABORT:  cmdTcpAbort();  break;
830
	case CMD_GET_LOCALIP: cmdGetLocalIP(); break;
831
	case CMD_NET_STATE:  cmdNetState();  break;
832
	case CMD_UDP_OPEN:   cmdUdpOpen();   break;
833
	case CMD_UDP_CLOSE:  cmdUdpClose();  break;
834
	case CMD_UDP_STATE:  cmdUdpState();  break;
835
	case CMD_UDP_SEND:   cmdUdpSend();   break;
836
	case CMD_UDP_RECV:   cmdUdpRecv();   break;
837
	case CMD_ICMP_SEND:  cmdIcmpSend();  break;
838
	case CMD_ICMP_RECV:  cmdIcmpRecv();  break;
839
	default:
840
		replyStatus(ERR_NOT_IMP);
841
		break;
842
	}
843
	paramBuf.clear(); // always clear params after a command
844
}
845
846
// DETECT (0x00)
847
// Params: none (strictly - rule 3)
848
// Reply: {0, 0x55, 2, caps, 0}
849
850
void UnapiNet::cmdDetect()
851
{
852
	// Strict like every other command: the driver issues DETECT twice
853
	// unconditionally, so a stray parameter block from a crashed
854
	// predecessor fails the first attempt, is cleared by it, and cannot
855
	// fail the second.
856
	if (!paramBuf.empty()) {
857
		replyStatus(ERR_INV_PARAM);
858
		return;
859
	}
860
	setResult(DetectResult{.caps =
861
		static_cast<uint8_t>(CAPS_BASE | (icmpAvailable ? CAP_ICMP : 0))});
862
}
863
864
// DNS_QUERY (0x01)
865
// Params: the hostname itself - the block length delimits it, no terminator
866
// Reply: {0, 0} lookup started, {0, 1, ip4} resolved immediately
867
868
void UnapiNet::cmdDnsQuery()
869
{
870
	// Form errors first, before the busy check; none of them touches the
871
	// DNS state or a running lookup. An empty block is an empty hostname,
872
	// and a block beyond the DNS name limit is "too long" (rule 3) - no
873
	// real name is that long, and resolving a truncated one would look
874
	// like success on the wrong name. An embedded NUL is malformed for a
875
	// kindred reason: the host resolver API speaks C strings and cannot
876
	// even carry the name past the NUL, so no lookup could ever see it -
877
	// rejecting it beats resolving a silently truncated prefix.
878
	if (paramBuf.empty() || paramBuf.size() > MAX_HOSTNAME ||
879
	    std::ranges::find(paramBuf, uint8_t(0)) != paramBuf.end()) {
880
		replyStatus(ERR_INV_PARAM);
881
		return;
882
	}
883
	{
884
		std::scoped_lock lock(dns.mutex);
885
		if (dns.status == DnsStatus::InProgress) {
886
			// The running lookup owns the DNS state.
887
			replyStatus(ERR_QUERY_EXISTS);
888
			return;
889
		}
890
	}
891
892
	std::string hostname(paramBuf.begin(), paramBuf.end());
893
894
	// Dotted-quad fast path: a strict a.b.c.d (four decimal octets) resolves
895
	// immediately and arms the sticky Complete state exactly as an
896
	// asynchronous success does. inet_pton is exactly that strict form.
897
	struct in_addr addr;
898
	if (inet_pton(AF_INET, hostname.c_str(), &addr) == 1) {
899
		uint32_t ip = ntohl(addr.s_addr);
900
		{
901
			std::scoped_lock lock(dns.mutex);
902
			dns.resolvedIp = ip;
903
			dns.status = DnsStatus::Complete;
904
		}
905
		setResult(DnsQueryResult{.ip = ip});
906
		return;
907
	}
908
909
	// Asynchronous resolution. Anything that is not a dotted quad goes to
910
	// the host resolver as-is: the device applies no syntax rules. The
911
	// lookup is handed to the persistent worker; the emulation thread
912
	// never waits on the resolver.
913
	{
914
		std::scoped_lock lock(dns.mutex);
915
		dns.status = DnsStatus::InProgress;
916
		dns.resolvedIp = 0;
917
		dns.request = std::move(hostname);
918
	}
919
	dns.cv.notify_one();
920
921
	const std::array<uint8_t, 2> started{ERR_OK, 0};
922
	setResult(started);
923
}
924
925
// The persistent DNS worker. It sleeps on the condition variable until
926
// DNS_QUERY queues a hostname (or the device shuts down), resolves it with
927
// the mutex released, and publishes the outcome only if dns.request still
928
// holds the name it resolved - the queued request doubles as the lookup's
929
// ownership token (Wouter's round-8 simplification). reset() clears the
930
// token, so a disowned lookup finishes into silence. The one soft spot is
931
// deliberate: re-querying the *same* name across a reset can be answered
932
// by the pre-reset lookup - it resolved the same string, so the answer is
933
// the answer.
934
void UnapiNet::dnsWorkerLoop()
935
{
936
	std::unique_lock lock(dns.mutex);
937
	while (true) {
938
		dns.cv.wait(lock, [&] { return !running || dns.request.has_value(); });
939
		if (!running) return;
940
		std::string hostname = *dns.request; // copy: 'request' stays engaged
941
		                                     // as the ownership token
942
		lock.unlock();
943
944
		struct addrinfo hints;
945
		memset(&hints, 0, sizeof(hints));
946
		hints.ai_family = AF_INET;
947
		hints.ai_socktype = SOCK_STREAM;
948
949
		struct addrinfo* res = nullptr;
950
		int err = getaddrinfo(hostname.c_str(), nullptr, &hints, &res);
951
		std::optional<uint32_t> ip;
952
		if (err == 0 && res != nullptr) {
953
			auto* addr4 = reinterpret_cast<struct sockaddr_in*>(res->ai_addr);
954
			// Host byte order here; the UA_B32 wire field re-emits it as
955
			// big-endian octets.
956
			ip = ntohl(addr4->sin_addr.s_addr);
957
		}
958
		if (res) freeaddrinfo(res);
959
960
		lock.lock();
961
		if (dns.request == hostname) { // else: disowned by a reset (and
962
			                           // possibly superseded), stay silent
963
			dns.request.reset();
964
			if (ip) {
965
				dns.resolvedIp = *ip;
966
				dns.status = DnsStatus::Complete;
967
			} else {
968
				dns.status = DnsStatus::Error;
969
			}
970
		}
971
	}
972
}
973
974
// DNS_STATUS (0x02)
975
// Params: none
976
// Reply: {0, 0} idle / {0, 1} in progress / {0, 2, ip4} complete /
977
//        {0, 0xFF, sub} failed - data, not a command error: the state is
978
//        sticky until the next DNS_QUERY or a reset
979
980
void UnapiNet::cmdDnsStatus()
981
{
982
	if (!paramBuf.empty()) {
983
		replyStatus(ERR_INV_PARAM);
984
		return;
985
	}
986
	std::scoped_lock lock(dns.mutex);
987
	switch (dns.status) {
988
	case DnsStatus::Complete:
989
		setResult(DnsStatusResult{.ip = dns.resolvedIp});
990
		break;
991
	case DnsStatus::Error:
992
		// A success reply carrying bad news: the lookup failed, the command
993
		// did not. Today the only sub-error the resolver reports is 'no
994
		// such host'.
995
		setResult(DnsStatusFailed{.sub = DNS_ERR_NO_SUCH_HOST});
996
		break;
997
	default: { // Idle (0) / InProgress (1)
998
		const std::array<uint8_t, 2> r{ERR_OK, static_cast<uint8_t>(dns.status)};
999
		setResult(r);
1000
		break;
1001
	}
1002
	}
1003
}
1004
1005
// TCP_OPEN (0x03)
1006
// Params: IP[4] + remote_port[2 LE] + local_port[2 LE] + timeout[2] + flags[1]
1007
// Flags bit 0: passive mode (listen). bit 1: resident. bits 2-7: must be 0.
1008
// Reply: {0, handle}
1009
1010
void UnapiNet::cmdTcpOpen()
1011
{
1012
	auto p = exactParams<TcpOpenParams>();
1013
	if (!p) {
1014
		replyStatus(ERR_INV_PARAM);
1015
		return;
1016
	}
1017
	if (p->flags & ~0x03) { // undefined flag bits must be 0
1018
		replyStatus(ERR_INV_PARAM);
1019
		return;
1020
	}
1021
	bool passive  = (p->flags & 0x01) != 0;
1022
	bool resident = (p->flags & 0x02) != 0;
1023
	uint32_t ip           = p->remoteIp;
1024
	uint16_t remotePort   = p->remotePort;
1025
	uint16_t localPortReq = p->localPort;
1026
	// p->timeout is accepted with any value and ignored (reserved)
1027
1028
	// The free-slot check precedes the host socket work; a slot consumed by
1029
	// a failure below is never published, i.e. released - ConnectFailed is
1030
	// for the asynchronous connect path only.
1031
	int h = allocTcpHandle();
1032
	if (h == INVALID_HANDLE) {
1033
		replyStatus(ERR_NO_FREE_CONN);
1034
		return;
1035
	}
1036
	auto& c = tcp[h];
1037
1038
	SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
1039
	if (s == OPENMSX_INVALID_SOCKET) {
1040
		replyStatus(ERR_NO_NETWORK);
1041
		return;
1042
	}
1043
1044
	sock_setIntOption(s, IPPROTO_TCP, TCP_NODELAY);
1045
	sock_setNonBlocking(s);
1046
1047
	TcpState newState = TcpState::Closed;
1048
	uint16_t localPort = 0;
1049
1050
	if (passive) {
1051
		// Passive: bind to local port, then listen
1052
		if (localPortReq == 0xFFFF) localPortReq = 0; // ephemeral, as in UDP_OPEN
1053
		// Allow address reuse (common for servers)
1054
		sock_setIntOption(s, SOL_SOCKET, SO_REUSEADDR);
1055
		sockaddr_in addr = sock_makeIPv4(INADDR_ANY, localPortReq);
1056
		if (bind(s, reinterpret_cast<struct sockaddr*>(&addr), sizeof(addr)) < 0) {
1057
			sock_close(s);
1058
			replyStatus(ERR_NO_NETWORK);
1059
			return;
1060
		}
1061
		if (listen(s, 1) < 0) {
1062
			sock_close(s);
1063
			replyStatus(ERR_NO_NETWORK);
1064
			return;
1065
		}
1066
		newState = TcpState::Listen;
1067
1068
		// Read back actual local port
1069
		::socklen_t alen = sizeof(addr);
1070
		if (getsockname(s, reinterpret_cast<struct sockaddr*>(&addr), &alen) == 0) {
1071
			localPort = ntohs(addr.sin_port);
1072
		} else {
1073
			localPort = localPortReq;
1074
		}
1075
	} else {
1076
		// Active connect. Remote IP/port are not semantically validated: a
1077
		// connect to 0.0.0.0 or port 0 is handed to the host and fails there.
1078
		sockaddr_in dest = sock_makeIPv4(ip, remotePort);
1079
		int ret = connect(s, reinterpret_cast<struct sockaddr*>(&dest), sizeof(dest));
1080
		if (ret == 0) {
1081
			newState = TcpState::Established;
1082
		} else {
1083
#ifdef _WIN32
1084
			int err = WSAGetLastError();
1085
			if (err == WSAEWOULDBLOCK) {
1086
#else
1087
			if (errno == EINPROGRESS) {
1088
#endif
1089
				newState = TcpState::SynSent;
1090
			} else {
1091
				sock_close(s);
1092
				replyStatus(ERR_NO_NETWORK);
1093
				return;
1094
			}
1095
		}
1096
		// Read back the local port the OS assigned
1097
		struct sockaddr_in local;
1098
		::socklen_t len = sizeof(local);
1099
		if (getsockname(s, reinterpret_cast<struct sockaddr*>(&local), &len) == 0) {
1100
			localPort = ntohs(local.sin_port);
1101
		}
1102
	}
1103
1104
	{
1105
		// Publish the connection in one go, with c.sock LAST: the socket thread
1106
		// thread only looks at a connection once its socket is valid, so it
1107
		// can never see a state without the matching address. Clearing the
1108
		// buffers here is what destroys a reused slot's sticky tail.
1109
		std::scoped_lock lock(c.mutex);
1110
		c.closeReason = CloseReason::None;
1111
		c.remoteIp    = ip;   // 0 = any; otherwise the socket thread filters on it
1112
		c.remotePort  = remotePort;
1113
		c.localPort   = localPort;
1114
		c.resident    = resident;
1115
		c.recvBuf.clear();
1116
		c.sendBuf.clear();
1117
		c.finSent = false;
1118
		c.tcpState    = newState;
1119
		c.sock        = s;
1120
	}
1121
	poke(); // start watching the new socket now
1122
1123
	setResult(OpenResult{.handle = static_cast<uint8_t>(h + 1)}); // wire handles are 1-based
1124
}
1125
1126
// TCP_SEND (0x04)
1127
// Params: handle[1] + len[2 LE] + data[len] - len states a fact about the
1128
// payload and must be exact; beyond MAX_TRANSFER it is ERR_INV_PARAM
1129
// Reply: {0}; ERR_BUFFER is all-or-nothing (retry the same command later)
1130
1131
void UnapiNet::cmdTcpSend()
1132
{
1133
	if (paramBuf.size() < sizeof(TcpSendParamHeader)) {
1134
		replyStatus(ERR_INV_PARAM);
1135
		return;
1136
	}
1137
	auto ph = fromBytes<TcpSendParamHeader>(paramBuf);
1138
	size_t len = ph.len;
1139
	if (paramBuf.size() != sizeof(TcpSendParamHeader) + len ||
1140
	    len > MAX_TRANSFER) {
1141
		replyStatus(ERR_INV_PARAM);
1142
		return;
1143
	}
1144
1145
	auto* cp = tcpForHandle(ph.handle);
1146
	if (!cp) {
1147
		replyStatus(ERR_NO_CONN);
1148
		return;
1149
	}
1150
	auto& c = *cp;
1151
1152
	// Check and act under the connection lock: without it the socket thread could
1153
	// close the socket between the check and the send.
1154
	const auto* data = paramBuf.data() + sizeof(TcpSendParamHeader);
1155
	bool failed = false;
1156
	bool queued = false; // bytes left in sendBuf for the socket thread
1157
	uint8_t status = ERR_OK;
1158
	{
1159
		std::scoped_lock lock(c.mutex);
1160
		SOCKET sd = c.sock;
1161
		if (sd == OPENMSX_INVALID_SOCKET) {
1162
			replyStatus(ERR_NO_CONN); // no connection at all
1163
			return;
1164
		}
1165
		// FinWait1 means the MSX already closed its side: no more sending.
1166
		if (c.tcpState != one_of(TcpState::Established, TcpState::CloseWait)) {
1167
			replyStatus(ERR_CONN_STATE); // a connection, in a state that forbids it
1168
			return;
1169
		}
1170
1171
		if (!c.sendBuf.empty()) {
1172
			// Data is already queued ahead of us: everything goes behind it,
1173
			// all or nothing - a rejected command was not partially queued
1174
			// and may be retried verbatim.
1175
			if (c.sendBuf.size() + len > MAX_SEND_BUF) {
1176
				status = ERR_BUFFER;
1177
			} else {
1178
				c.sendBuf.insert(c.sendBuf.end(), data, data + len);
1179
				queued = true;
1180
			}
1181
		} else {
1182
			// Nothing queued ahead of us: hand it straight to the kernel. The
1183
			// socket is non-blocking, so this cannot stall the emulation
1184
			// thread. Whatever the kernel would not take goes to the socket
1185
			// thread, which drains it as the peer makes room - and it always
1186
			// fits, because len <= MAX_TRANSFER <= MAX_SEND_BUF.
1187
			auto r = netSend(sd, data, len);
1188
			if (r || r.error() == IoError::WouldBlock) {
1189
				size_t sent = r.value_or(0);
1190
				c.sendBuf.insert(c.sendBuf.end(), data + sent, data + len);
1191
				queued = sent < len;
1192
			} else {
1193
				failed = true;
1194
			}
1195
		}
1196
	}
1197
	if (failed) {
1198
		// A socket write failure resets the connection; the reply reports
1199
		// the state change, not a private send error code.
1200
		requestClose(c, CloseReason::ConnectionReset);
1201
		replyStatus(ERR_CONN_STATE);
1202
		return;
1203
	}
1204
	if (queued) poke(); // arm this socket for write now, not at the
1205
	                    // next select() timeout
1206
	replyStatus(status);
1207
}
1208
1209
// TCP_RECV (0x05)
1210
// Params: handle[1] + maxlen[2 LE] - maxlen is a ceiling request: beyond
1211
// MAX_TRANSFER it is clamped, not rejected; 0 reads nothing and consumes
1212
// nothing
1213
// Reply: {0, actual_len[2 LE], data[actual_len]}
1214
1215
void UnapiNet::cmdTcpRecv()
1216
{
1217
	auto p = exactParams<TcpRecvParams>();
1218
	if (!p) {
1219
		replyStatus(ERR_INV_PARAM);
1220
		return;
1221
	}
1222
	auto* cp = tcpForHandle(p->handle);
1223
	if (!cp) {
1224
		replyStatus(ERR_NO_CONN);
1225
		return;
1226
	}
1227
	auto& c = *cp;
1228
	// Answers for any in-range handle, open or closed: the undrained tail
1229
	// of a dead connection stays readable until TCP_OPEN reuses the slot.
1230
1231
	size_t maxlen = std::min(static_cast<size_t>(p->maxlen), MAX_TRANSFER);
1232
1233
	// Build the whole result under the connection lock: the length in the
1234
	// header and the bytes copied behind it must agree, and requestClose()
1235
	// runs on another thread. setResult() does the resultPos bookkeeping;
1236
	// appending the payload afterwards doesn't disturb it.
1237
	// Nothing allocates while the lock is held: resultBuf was reserved in
1238
	// the constructor and avail <= MAX_TRANSFER.
1239
	std::scoped_lock lock(c.mutex);
1240
	auto avail = static_cast<uint16_t>(std::min(maxlen, c.recvBuf.size()));
1241
	setResult(TcpRecvResultHeader{.actualLen = avail});
1242
	resultBuf.insert(resultBuf.end(), c.recvBuf.begin(),
1243
	                 c.recvBuf.begin() + avail);
1244
	c.recvBuf.erase(c.recvBuf.begin(), c.recvBuf.begin() + avail);
1245
}
1246
1247
// TCP_CLOSE (0x06)
1248
// Params: handle[1]; 0 = close all transient (succeeds even with none open)
1249
// Reply: {0}
1250
1251
void UnapiNet::cmdTcpClose()
1252
{
1253
	if (paramBuf.size() != 1) {
1254
		replyStatus(ERR_INV_PARAM);
1255
		return;
1256
	}
1257
	int h = paramBuf[0];
1258
1259
	if (h == 0) {
1260
		// Close all transient connections. Gracefully: data the MSX has
1261
		// already handed us must not be thrown away.
1262
		for (auto& c : tcp) {
1263
			if (!c.resident && c.sock != OPENMSX_INVALID_SOCKET) {
1264
				gracefulClose(c);
1265
			}
1266
		}
1267
		replyStatus(ERR_OK);
1268
		return;
1269
	}
1270
1271
	auto* cp = tcpForHandle(h);
1272
	if (!cp || cp->sock == OPENMSX_INVALID_SOCKET) {
1273
		replyStatus(ERR_NO_CONN);
1274
		return;
1275
	}
1276
	gracefulClose(*cp);
1277
	replyStatus(ERR_OK);
1278
}
1279
1280
// TCP_STATE (0x07)
1281
// Params: handle[1]
1282
// Reply: {0, state[1], avail[2 LE], close_reason[1], remote_IP[4],
1283
//         remote_port[2 LE], local_port[2 LE]} - answers for any in-range
1284
// handle: a closed slot keeps its final state and close reason, a
1285
// never-used one shows closeReason 1 (NeverUsed) and zeros
1286
1287
void UnapiNet::cmdTcpState()
1288
{
1289
	if (paramBuf.size() != 1) {
1290
		replyStatus(ERR_INV_PARAM);
1291
		return;
1292
	}
1293
	auto* cp = tcpForHandle(paramBuf[0]);
1294
	if (!cp) {
1295
		replyStatus(ERR_NO_CONN);
1296
		return;
1297
	}
1298
	auto& c = *cp;
1299
1300
	TcpStateResult r{};
1301
	{
1302
		// One coherent snapshot: the socket thread publishes the state
1303
		// and the endpoint metadata together under this same lock.
1304
		std::scoped_lock lock(c.mutex);
1305
		r.state       = static_cast<uint8_t>(c.tcpState.load());
1306
		r.avail       = static_cast<uint16_t>(
1307
			std::min(c.recvBuf.size(), static_cast<size_t>(0xFFFF)));
1308
		r.closeReason = static_cast<uint8_t>(c.closeReason);
1309
		r.remoteIp    = c.remoteIp;
1310
		r.remotePort  = c.remotePort;
1311
		r.localPort   = c.localPort;
1312
	}
1313
	setResult(r);
1314
}
1315
1316
// TCP_ABORT (0x08)
1317
// Params: handle[1]; 0 = abort all transient (UNAPI's TCPIP_TCP_ABORT B=0)
1318
// Reply: {0} - the aborted slot's tail stays readable (sticky-slot rule)
1319
1320
void UnapiNet::cmdTcpAbort()
1321
{
1322
	if (paramBuf.size() != 1) {
1323
		replyStatus(ERR_INV_PARAM);
1324
		return;
1325
	}
1326
	int h = paramBuf[0];
1327
1328
	if (h == 0) {
1329
		// Abort every transient connection; succeeds even with none open.
1330
		for (auto& c : tcp) {
1331
			if (!c.resident && c.sock != OPENMSX_INVALID_SOCKET) {
1332
				requestClose(c, CloseReason::Aborted);
1333
			}
1334
		}
1335
		replyStatus(ERR_OK);
1336
		return;
1337
	}
1338
1339
	auto* cp = tcpForHandle(h);
1340
	if (!cp || cp->sock == OPENMSX_INVALID_SOCKET) {
1341
		replyStatus(ERR_NO_CONN);
1342
		return;
1343
	}
1344
	requestClose(*cp, CloseReason::Aborted);
1345
	replyStatus(ERR_OK);
1346
}
1347
1348
// GET_LOCALIP (0x0D)
1349
// Params: none
1350
// Reply: {0, ip4} - 0.0.0.0 when the host lookup fails, still a success
1351
1352
void UnapiNet::cmdGetLocalIP()
1353
{
1354
	if (!paramBuf.empty()) {
1355
		replyStatus(ERR_INV_PARAM);
1356
		return;
1357
	}
1358
	setResult(GetLocalIpResult{.ip = sock_localIPv4()});
1359
}
1360
1361
// NET_STATE (0x0E)
1362
// Params: none
1363
// Reply: {0, 2} - always 'open': the host's network is ours
1364
1365
void UnapiNet::cmdNetState()
1366
{
1367
	if (!paramBuf.empty()) {
1368
		replyStatus(ERR_INV_PARAM);
1369
		return;
1370
	}
1371
	const std::array<uint8_t, 2> r{ERR_OK, 2};
1372
	setResult(r);
1373
}
1374
1375
// UDP: handle management
1376
1377
int UnapiNet::allocUdpHandle()
1378
{
1379
	for (int i = 0; i < MAX_UDP; i++) {
1380
		if (udp[i].sock == OPENMSX_INVALID_SOCKET) {
1381
			return i;
1382
		}
1383
	}
1384
	return INVALID_HANDLE;
1385
}
1386
1387
// Direct close. Only safe with the socket thread stopped (destructor).
1388
void UnapiNet::closeUdp(UdpConnection& u)
1389
{
1390
	std::scoped_lock lock(u.mutex);
1391
	if (SOCKET sd = u.sock; sd != OPENMSX_INVALID_SOCKET) {
1392
		sock_close(sd);
1393
		u.sock = OPENMSX_INVALID_SOCKET;
1394
	}
1395
	u.localPort = 0;
1396
	u.resident = false;
1397
	u.recvQueue.clear();
1398
}
1399
1400
// UDP_OPEN (0x09)
1401
// Params: local_port[2 LE] - 0xFFFF requests an ephemeral port; 0 reaches
1402
// the host bind() where it also yields one, by a different route
1403
// Reply: {0, handle}
1404
1405
void UnapiNet::cmdUdpOpen()
1406
{
1407
	auto p = exactParams<UdpOpenParams>();
1408
	if (!p) {
1409
		replyStatus(ERR_INV_PARAM);
1410
		return;
1411
	}
1412
	uint16_t localPort = p->localPort;
1413
1414
	// Free-slot check before the host socket work, as in TCP_OPEN.
1415
	int h = allocUdpHandle();
1416
	if (h == INVALID_HANDLE) {
1417
		replyStatus(ERR_NO_FREE_CONN);
1418
		return;
1419
	}
1420
	auto& u = udp[h];
1421
1422
	SOCKET s = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
1423
	if (s == OPENMSX_INVALID_SOCKET) {
1424
		replyStatus(ERR_NO_NETWORK);
1425
		return;
1426
	}
1427
1428
	sock_setNonBlocking(s);
1429
1430
	// Enable broadcast - required by UNAPI clients that do LAN service
1431
	// discovery via sendto(255.255.255.255). Real-hardware UNAPI stacks
1432
	// (GR8NET, Obsonet) allow it implicitly; the BSD socket layer needs
1433
	// the explicit opt-in.
1434
	sock_setIntOption(s, SOL_SOCKET, SO_BROADCAST);
1435
1436
	sockaddr_in addr = sock_makeIPv4(INADDR_ANY, localPort == 0xFFFF ? 0 : localPort);
1437
	if (bind(s, reinterpret_cast<struct sockaddr*>(&addr), sizeof(addr)) < 0) {
1438
		// The requested port may be taken by the HOST itself - Windows'
1439
		// time service owns UDP 123, which is exactly what an SNTP client
1440
		// asks for - a collision that cannot exist on the real-hardware
1441
		// UNAPI stacks this bridge stands in for, which own their whole
1442
		// IP. A client's local port carries no meaning for the peer, so
1443
		// fall back to an ephemeral one rather than refuse a socket the
1444
		// program needs.
1445
		addr.sin_port = 0;
1446
		if (bind(s, reinterpret_cast<struct sockaddr*>(&addr), sizeof(addr)) < 0) {
1447
			sock_close(s);
1448
			replyStatus(ERR_NO_NETWORK);
1449
			return;
1450
		}
1451
	}
1452
1453
	// Read back the actual local port
1454
	const auto boundPort = [&]() -> uint16_t {
1455
		::socklen_t alen = sizeof(addr);
1456
		return (getsockname(s, reinterpret_cast<struct sockaddr*>(&addr), &alen) == 0)
1457
			? ntohs(addr.sin_port)
1458
			: localPort;
1459
	}();
1460
1461
	{
1462
		// Publish with u.sock last (see the threading contract in the header).
1463
		std::scoped_lock lock(u.mutex);
1464
		u.localPort = boundPort;
1465
		u.resident  = false;
1466
		u.recvQueue.clear();
1467
		u.sock      = s;
1468
	}
1469
	poke(); // start watching the new socket now
1470
1471
	setResult(OpenResult{.handle = static_cast<uint8_t>(h + 1)}); // wire handles are 1-based
1472
}
1473
1474
// UDP_CLOSE (0x0A)
1475
// Params: handle[1]; 0 = close every open UDP socket (all are transient)
1476
// Reply: {0} - queued datagrams are discarded with the socket
1477
1478
void UnapiNet::cmdUdpClose()
1479
{
1480
	if (paramBuf.size() != 1) {
1481
		replyStatus(ERR_INV_PARAM);
1482
		return;
1483
	}
1484
	int h = paramBuf[0];
1485
1486
	if (h == 0) {
1487
		for (auto& u : udp) {
1488
			if (!u.resident && u.sock != OPENMSX_INVALID_SOCKET) {
1489
				requestClose(u);
1490
			}
1491
		}
1492
		replyStatus(ERR_OK);
1493
		return;
1494
	}
1495
1496
	auto* up = udpForHandle(h);
1497
	if (!up || up->sock == OPENMSX_INVALID_SOCKET) {
1498
		replyStatus(ERR_NO_CONN);
1499
		return;
1500
	}
1501
	requestClose(*up);
1502
	replyStatus(ERR_OK);
1503
}
1504
1505
// UDP_STATE (0x0B)
1506
// Params: handle[1]
1507
// Reply: {0, first_dgram_size[2 LE]} - 0 if the queue is empty, or if
1508
// an empty datagram heads it; UDP_RECV until ERR_NO_DATA is the UNAPI
1509
// idiom that tells the two apart (TCPIP_UDP_STATE's own advice)
1510
1511
void UnapiNet::cmdUdpState()
1512
{
1513
	if (paramBuf.size() != 1) {
1514
		replyStatus(ERR_INV_PARAM);
1515
		return;
1516
	}
1517
	auto* up = udpForHandle(paramBuf[0]);
1518
	if (!up || up->sock == OPENMSX_INVALID_SOCKET) {
1519
		replyStatus(ERR_NO_CONN);
1520
		return;
1521
	}
1522
	auto& u = *up;
1523
1524
	const auto size = [&]() -> uint16_t {
1525
		std::scoped_lock lock(u.mutex);
1526
		if (u.recvQueue.empty()) return 0;
1527
		return static_cast<uint16_t>(
1528
			std::min(u.recvQueue.front().data.size(),
1529
			         static_cast<size_t>(0xFFFF)));
1530
	}();
1531
	setResult(UdpStateResult{.firstDgramSize = size});
1532
}
1533
1534
// UDP_SEND (0x0C)
1535
// Params: handle[1] + dest_IP[4] + dest_port[2 LE] + len[2 LE] + data[len]
1536
// Reply: {0} - length 0 sends an empty datagram; an oversized one is not a
1537
// form error: the host refuses it ({2}) and the socket stays usable
1538
1539
void UnapiNet::cmdUdpSend()
1540
{
1541
	if (paramBuf.size() < sizeof(UdpSendParamHeader)) {
1542
		replyStatus(ERR_INV_PARAM);
1543
		return;
1544
	}
1545
	auto ph = fromBytes<UdpSendParamHeader>(paramBuf);
1546
	size_t len = ph.len;
1547
	if (paramBuf.size() != sizeof(UdpSendParamHeader) + len) {
1548
		replyStatus(ERR_INV_PARAM);
1549
		return;
1550
	}
1551
1552
	auto* up = udpForHandle(ph.handle);
1553
	if (!up || up->sock == OPENMSX_INVALID_SOCKET) {
1554
		replyStatus(ERR_NO_CONN);
1555
		return;
1556
	}
1557
	auto& u = *up;
1558
1559
	sockaddr_in dest = sock_makeIPv4(ph.destIp, ph.destPort);
1560
	const char* data = reinterpret_cast<const char*>(
1561
		paramBuf.data() + sizeof(UdpSendParamHeader));
1562
	int n;
1563
	{
1564
		// Snapshot the socket under the lock: the socket thread may be closing it.
1565
		std::scoped_lock lock(u.mutex);
1566
		SOCKET sd = u.sock;
1567
		if (sd == OPENMSX_INVALID_SOCKET) {
1568
			replyStatus(ERR_NO_CONN);
1569
			return;
1570
		}
1571
		n = sendto(sd, data, static_cast<int>(len), 0,
1572
		           reinterpret_cast<struct sockaddr*>(&dest), sizeof(dest));
1573
	}
1574
	// A refused or short kernel write leaves the socket open and usable.
1575
	replyStatus(n == static_cast<int>(len) ? ERR_OK : ERR_NO_NETWORK);
1576
}
1577
1578
// UDP_RECV (0x0F)
1579
// Params: handle[1] + maxlen[2 LE] - maxlen 0 is TCPIP_UDP_RCV's DE=0,
1580
// a deliberate discard: consume the head, copy nothing
1581
// Reply: {0, src_IP[4], src_port[2 LE], actual_len[2 LE],
1582
//         data[min(actual_len, maxlen)]} - actual_len is the datagram's
1583
// size as received (UNAPI's BC may exceed the bytes retrieved)
1584
1585
void UnapiNet::cmdUdpRecv()
1586
{
1587
	auto p = exactParams<UdpRecvParams>();
1588
	if (!p) {
1589
		replyStatus(ERR_INV_PARAM);
1590
		return;
1591
	}
1592
	uint16_t maxlen = p->maxlen;
1593
	auto* up = udpForHandle(p->handle);
1594
	if (!up || up->sock == OPENMSX_INVALID_SOCKET) {
1595
		replyStatus(ERR_NO_CONN);
1596
		return;
1597
	}
1598
	auto& u = *up;
1599
1600
	std::optional<UdpDatagram> dg;
1601
	{
1602
		std::scoped_lock lock(u.mutex);
1603
		if (!u.recvQueue.empty()) {
1604
			dg = std::move(u.recvQueue.front());
1605
			u.recvQueue.pop_front();
1606
		}
1607
	}
1608
	if (!dg) {
1609
		replyStatus(ERR_NO_DATA);
1610
		return;
1611
	}
1612
1613
	// The head datagram is consumed whole; the reply reports its size as
1614
	// received and carries the first min(size, maxlen) bytes - whatever
1615
	// exceeds maxlen is discarded with it. This is what lets the driver
1616
	// report TCPIP_UDP_RCV's BC ("size as it was received, which may be
1617
	// larger than the number of bytes actually retrieved") without a
1618
	// second command.
1619
	auto copied = std::min(static_cast<size_t>(maxlen), dg->data.size());
1620
	setResult(UdpRecvResultHeader{
1621
			.srcIp     = dg->srcIp,
1622
			.srcPort   = dg->srcPort,
1623
			.actualLen = static_cast<uint16_t>(dg->data.size())},
1624
		std::span<const uint8_t>(dg->data.data(), copied));
1625
}
1626
1627
// ICMP Echo (ping)
1628
//
1629
// Uses the Windows IcmpSendEcho API (doesn't require admin). A worker
1630
// thread handles the (blocking) call and pushes replies to a queue that
1631
// the MSX polls via ICMP_RECV. The worker only runs when the capability
1632
// latched at device start; elsewhere the ICMP commands answer ERR_NOT_IMP.
1633
1634
void UnapiNet::icmpWorkerLoop()
1635
{
1636
#ifdef _WIN32
1637
	HANDLE hIcmp = icmpChannel; // opened (and the capability latched) at device start
1638
1639
	std::unique_lock lock(icmpMutex);
1640
	while (true) {
1641
		icmpCv.wait(lock, [&] { return !running || icmpPending; });
1642
		if (!running) return;
1643
		icmpPending = false;
1644
		IcmpRequest req = icmpRequest; // copy out under the lock: a new
1645
		                               // ICMP_SEND cannot tear it
1646
		uint32_t gen = icmpGeneration;
1647
		lock.unlock();
1648
1649
		std::vector<uint8_t> payload(req.dataLen);
1650
		for (size_t i = 0; i < payload.size(); i++)
1651
			payload[i] = static_cast<uint8_t>(i);
1652
1653
		unsigned long replySize = sizeof(ICMP_ECHO_REPLY) + req.dataLen + 8;
1654
		std::vector<uint8_t> replyBuf(replySize);
1655
1656
		IP_OPTION_INFORMATION opt = {};
1657
		opt.Ttl = req.ttl ? req.ttl : 255;
1658
1659
		unsigned long ret = IcmpSendEcho(hIcmp,
1660
										 htonl(req.dstIp),
1661
										 payload.empty() ? nullptr : payload.data(),
1662
										 static_cast<unsigned short>(payload.size()),
1663
										 &opt,
1664
										 replyBuf.data(),
1665
										 replySize,
1666
										 2000);
1667
1668
		std::optional<IcmpReply> r;
1669
		if (ret > 0) {
1670
			auto* reply = reinterpret_cast<ICMP_ECHO_REPLY*>(replyBuf.data());
1671
			if (reply->Status == IP_SUCCESS) {
1672
				r = IcmpReply{
1673
					.srcIp      = ntohl(reply->Address),
1674
					.ttl        = reply->Options.Ttl,
1675
					.identifier = req.identifier,
1676
					.sequence   = req.sequence,
1677
					.dataLen    = reply->DataSize};
1678
			}
1679
		}
1680
1681
		lock.lock(); // held again for the publish and the next wait
1682
		if (r && icmpGeneration == gen) { // else: a reset intervened
1683
			                              // while the echo was in flight
1684
			if (icmpReplies.size() >= MAX_ICMP_QUEUE) {
1685
				// Drop the OLDEST: a reply nobody polled for is worth
1686
				// less than the one the current program is waiting for.
1687
				icmpReplies.pop_front();
1688
			}
1689
			icmpReplies.push_back(*r);
1690
		}
1691
	}
1692
#endif
1693
}
1694
1695
// ICMP_SEND (0x11)
1696
// Params: IP[4] + TTL[1] + ID[2 LE] + SEQ[2 LE] + len[2 LE]
1697
// Reply: {0} - acknowledges queueing to the worker, not delivery: an
1698
// unreachable destination simply never yields a reply
1699
1700
void UnapiNet::cmdIcmpSend()
1701
{
1702
	auto p = exactParams<IcmpSendParams>();
1703
	if (!p) {
1704
		replyStatus(ERR_INV_PARAM);
1705
		return;
1706
	}
1707
	if (!icmpAvailable) {
1708
		// The capability bit is enforced, not decorative.
1709
		replyStatus(ERR_NOT_IMP);
1710
		return;
1711
	}
1712
	{
1713
		// Under the lock: the worker may be copying the previous request
1714
		// out right now.
1715
		std::scoped_lock lock(icmpMutex);
1716
		icmpRequest.dstIp      = p->dstIp;
1717
		icmpRequest.ttl        = p->ttl;
1718
		icmpRequest.identifier = p->identifier;
1719
		icmpRequest.sequence   = p->sequence;
1720
		icmpRequest.dataLen    = std::min<uint16_t>(p->len, 512); // echo-size clamp
1721
		icmpPending = true;
1722
	}
1723
	icmpCv.notify_one();
1724
	replyStatus(ERR_OK);
1725
}
1726
1727
// ICMP_RECV (0x12)
1728
// Params: none
1729
// Reply: {0, src_IP[4], TTL[1], ID[2 LE], SEQ[2 LE], len[2 LE]} - consumes
1730
// exactly one queued reply; matching id/seq to requests is the driver's job
1731
1732
void UnapiNet::cmdIcmpRecv()
1733
{
1734
	if (!paramBuf.empty()) {
1735
		replyStatus(ERR_INV_PARAM);
1736
		return;
1737
	}
1738
	if (!icmpAvailable) {
1739
		replyStatus(ERR_NOT_IMP);
1740
		return;
1741
	}
1742
1743
	std::optional<IcmpReply> r;
1744
	{
1745
		std::scoped_lock lock(icmpMutex);
1746
		if (!icmpReplies.empty()) {
1747
			r = icmpReplies.front();
1748
			icmpReplies.pop_front();
1749
		}
1750
	}
1751
	if (!r) {
1752
		replyStatus(ERR_NO_DATA);
1753
		return;
1754
	}
1755
1756
	setResult(IcmpRecvResult{
1757
		.srcIp      = r->srcIp,
1758
		.ttl        = r->ttl,
1759
		.identifier = r->identifier,
1760
		.sequence   = r->sequence,
1761
		.dataLen    = r->dataLen});
1762
}
1763
1764
// Serialization (save state)
1765
// We don't serialize sockets or network state.
1766
// On restore, connections are lost.
1767
1768
template<typename Archive>
1769
void UnapiNet::serialize(Archive& ar, unsigned /*version*/)
1770
{
1771
	ar.template serializeBase<MSXDevice>(*this);
1772
	// Don't serialize network state - connections are lost on save/load
1773
}
1774
1775
INSTANTIATE_SERIALIZE_METHODS(UnapiNet);
1776
REGISTER_MSXDEVICE(UnapiNet, "UnapiNet");
1777
1778
} // namespace openmsx