OXYGENHUB
◆ main openmsx / src / network / UnapiNet.hh
12.4 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
1
#ifndef UNAPINET_HH
2
#define UNAPINET_HH
3
4
#include "MSXDevice.hh"
5
#include "Socket.hh"
6
#include "UnapiNetWire.hh"
7
8
#include <array>
9
#include <atomic>
10
#include <chrono>
11
#include <concepts>
12
#include <condition_variable>
13
#include <cstdint>
14
#include <deque>
15
#include <mutex>
16
#include <optional>
17
#include <span>
18
#include <string>
19
#include <thread>
20
#include <vector>
21
22
// UnapiNet - MSX-UNAPI TCP/IP bridge device
23
//
24
// I/O ports 0x28 (command, write-only) and 0x29 (data). Same range as the
25
// DenYoNet - both are UNAPI Ethernet bridges and don't coexist.
26
// Bridge between the MSX and BSD sockets on the host, speaking protocol v2
27
// (see network/protocol-v2.md): every reply starts with a status byte.
28
29
namespace openmsx {
30
31
class UnapiNet final : public MSXDevice
32
{
33
public:
34
	explicit UnapiNet(const DeviceConfig& config);
35
	~UnapiNet() override;
36
37
	void reset(EmuTime time) override;
38
	[[nodiscard]] byte readIO(uint16_t port, EmuTime time) override;
39
	[[nodiscard]] byte peekIO(uint16_t port, EmuTime time) const override;
40
	void writeIO(uint16_t port, byte value, EmuTime time) override;
41
42
	template<typename Archive>
43
	void serialize(Archive& ar, unsigned version);
44
45
private:
46
	// Keep the host socket subsystem initialized for this device's lifetime
47
	// (WSAStartup/WSACleanup on Windows). Empty type -> [[no_unique_address]].
48
	[[no_unique_address]] SocketActivator socketActivator;
49
50
	// --- I/O protocol state ---
51
	// The command port is write-only in v2: every reply, its status byte
52
	// included, is read from the data port. Whether a reply is pending is
53
	// resultPos < resultBuf.size().
54
55
	// Parameter buffer (written to 0x29 before the command). Capped at one
56
	// byte past the largest legal block, so a truncated block always fails
57
	// the receiving command's size check (rule 3).
58
	std::vector<uint8_t> paramBuf;
59
60
	// Result buffer (read from 0x29 after the command)
61
	std::vector<uint8_t> resultBuf;
62
	size_t resultPos = 0;
63
64
	// --- TCP connections ---
65
	static constexpr int MAX_TCP = 4;
66
	// Internal handles are 0-based array indices; only the wire protocol is
67
	// 1-based. Conversion happens in tcpForHandle()/udpForHandle() and in the
68
	// OPEN replies, nowhere else.
69
	static constexpr int INVALID_HANDLE = -1;
70
71
	// TCP states (UNAPI spec wire values)
72
	enum class TcpState : uint8_t {
73
		Closed      = 0,
74
		Listen      = 1,
75
		SynSent     = 2,
76
		SynRecv     = 3,
77
		Established = 4,
78
		FinWait1    = 5,
79
		FinWait2    = 6,
80
		CloseWait   = 7,
81
		Closing     = 8,
82
		LastAck     = 9,
83
		TimeWait    = 10,
84
	};
85
86
	// Wire-defined TCP close-reason codes (read back by the UNAPI TSR).
87
	enum class CloseReason : uint8_t {
88
		None = 0, NeverUsed = 1, ClosedByUser = 2,
89
		Aborted = 3, ConnectionReset = 4, ConnectFailed = 6,
90
	};
91
92
	// Threading contract between the emulation thread (readIO/writeIO and
93
	// everything they call) and the socket thread (socketLoop):
94
	//
95
	// * Only the socket thread ever calls sock_close(). requestClose() (any
96
	//   thread) invalidates the connection's socket immediately - so the MSX
97
	//   can reuse the handle at once - and hands the raw fd to socksToClose;
98
	//   the socket thread closes it at the top of its next pass. The fd therefore
99
	//   stays open until then, so its number cannot be recycled while another
100
	//   thread is still sitting on it inside select()/recv().
101
	//   closeTcp()/closeUdp() close directly and are only for the destructor,
102
	//   which has already joined the socket thread.
103
	// * 'mutex' guards the buffers AND the endpoint metadata, so that TCP_STATE
104
	//   reads a coherent snapshot and the accept path publishes the state and
105
	//   the address together. The socket thread re-checks that the socket is still
106
	//   the one it was watching before publishing anything, so it can never
107
	//   act on a connection the MSX has closed and reopened underneath it.
108
	// * Hold at most one connection mutex at a time, and never across
109
	//   select()/accept()/recv()/connect(). Non-blocking send/shutdown/close
110
	//   under the lock are fine.
111
	struct TcpConnection {
112
		std::atomic<SOCKET> sock{OPENMSX_INVALID_SOCKET};
113
		std::atomic<TcpState> tcpState{TcpState::Closed};
114
		// FinWait1 = the MSX called TCP_CLOSE. The FIN goes out once sendBuf has
115
		// drained (finSent), and the connection is dropped when the peer closes
116
		// too, or when closeDeadline passes and it never does.
117
		bool finSent = false; // guarded by 'mutex'
118
		std::chrono::steady_clock::time_point closeDeadline; // guarded by 'mutex'
119
		CloseReason closeReason = CloseReason::NeverUsed; // guarded by 'mutex'
120
		uint32_t remoteIp = 0;    // guarded by 'mutex'
121
		uint16_t remotePort = 0;  // guarded by 'mutex'
122
		uint16_t localPort = 0;   // guarded by 'mutex'
123
		bool     resident = false; // emulation thread only
124
		std::deque<uint8_t> recvBuf;  // guarded by 'mutex'
125
		std::vector<uint8_t> sendBuf; // guarded by 'mutex'
126
		std::mutex mutex;
127
	};
128
	std::array<TcpConnection, MAX_TCP> tcp; // indexed by 0-based internal handle
129
130
	// --- UDP connections ---
131
	static constexpr int MAX_UDP = 4;
132
133
	struct UdpDatagram {
134
		uint32_t srcIp = 0;
135
		uint16_t srcPort = 0;
136
		std::vector<uint8_t> data;
137
	};
138
139
	struct UdpConnection { // same threading contract as TcpConnection above
140
		std::atomic<SOCKET> sock{OPENMSX_INVALID_SOCKET};
141
		uint16_t localPort = 0;   // guarded by 'mutex'
142
		bool     resident = false; // emulation thread only; nothing sets it
143
		                           // today (v2: every UDP socket is transient)
144
		std::deque<UdpDatagram> recvQueue; // guarded by 'mutex'
145
		std::mutex mutex;
146
	};
147
	std::array<UdpConnection, MAX_UDP> udp;
148
149
	// --- ICMP echo ---
150
	// The capability is latched ONCE at device start (platform support
151
	// compiled in AND the channel opening successfully); DETECT advertises
152
	// it, the ICMP commands are gated on it, and reset() does not re-probe.
153
	bool icmpAvailable = false;
154
	void* icmpChannel = nullptr; // Windows HANDLE from IcmpCreateFile()
155
156
	struct IcmpReply {
157
		uint32_t srcIp = 0;
158
		uint8_t  ttl = 0;
159
		uint16_t identifier = 0;
160
		uint16_t sequence = 0;
161
		uint16_t dataLen = 0;
162
	};
163
	std::deque<IcmpReply> icmpReplies; // guarded by icmpMutex
164
	// reset() bumps the generation so an echo still in flight inside the
165
	// worker cannot repopulate the queue reset just cleared.
166
	uint32_t icmpGeneration = 0;       // guarded by icmpMutex
167
	std::mutex icmpMutex; // protects icmpReplies, icmpGeneration,
168
	                      // icmpRequest, icmpPending
169
	std::condition_variable icmpCv; // a request was queued, or shutdown
170
	std::thread icmpWorker;
171
	bool icmpPending = false; // guarded by icmpMutex
172
	// ICMP request for worker to handle (guarded by icmpMutex: the worker
173
	// copies it out under the lock, so a new ICMP_SEND cannot tear it)
174
	struct IcmpRequest {
175
		uint32_t dstIp = 0;
176
		uint8_t  ttl = 0;
177
		uint16_t identifier = 0;
178
		uint16_t sequence = 0;
179
		uint16_t dataLen = 0;
180
	} icmpRequest;
181
182
	// --- Async DNS ---
183
	enum class DnsStatus : uint8_t { Idle = 0, InProgress = 1, Complete = 2, Error = 3 };
184
	// One persistent worker thread (dnsWorkerLoop) serves DNS_QUERY: the
185
	// emulation thread queues the hostname in 'request' and never blocks on
186
	// the resolver. getaddrinfo() has no reliable cancellation, so a lookup
187
	// in flight is disowned rather than stopped: 'request' stays engaged
188
	// while its lookup runs and doubles as the ownership token - the worker
189
	// publishes its outcome (and clears 'request') only if 'request' still
190
	// holds the name it resolved. reset() clears 'request', so a lookup
191
	// that finishes after a reset cannot overwrite the state the reset
192
	// established. Invariant: request.has_value() iff status == InProgress.
193
	struct {
194
		DnsStatus status = DnsStatus::Idle; // guarded by 'mutex'
195
		uint32_t resolvedIp = 0;            // guarded by 'mutex'
196
		std::optional<std::string> request; // guarded by 'mutex'
197
		std::mutex mutex;
198
		std::condition_variable cv; // a request was queued, or shutdown
199
	} dns;
200
	std::thread dnsThread; // the persistent worker
201
	void dnsWorkerLoop();
202
203
	// --- Network socket thread ---
204
	// (One loop serves all sockets: it receives, flushes queued sends,
205
	// completes non-blocking connects and performs every close.)
206
	std::thread sockThread;
207
	std::atomic<bool> running{false};
208
	void socketLoop();
209
210
	// Loopback wake pair. The socket thread parks in select() for up to
211
	// 100 ms at a time; wakeRecv sits in every read set and poke() sends
212
	// it one byte, so new work from the emulation thread - a queued send,
213
	// a fresh socket to watch, a handed-over close, shutdown - is noticed
214
	// immediately instead of at the next timeout. (A UDP pair bound to
215
	// 127.0.0.1, because Windows select() only takes sockets - the POSIX
216
	// self-pipe trick spelled with what both platforms have.) The
217
	// constructor either creates the pair or throws MSXException, so both
218
	// sockets are always valid here: poke() is a guaranteed wake-up, and
219
	// the socket loop always has at least this fd in its select() set.
220
	SOCKET wakeRecv = OPENMSX_INVALID_SOCKET;
221
	SOCKET wakeSend = OPENMSX_INVALID_SOCKET;
222
	void poke();
223
224
	// --- Command processing ---
225
	void processCmd(uint8_t cmd);
226
	void cmdDetect();
227
	void cmdDnsQuery();
228
	void cmdDnsStatus();
229
	void cmdTcpOpen();
230
	void cmdTcpSend();
231
	void cmdTcpRecv();
232
	void cmdTcpClose();
233
	void cmdTcpState();
234
	void cmdTcpAbort();
235
	void cmdGetLocalIP();
236
	void cmdNetState();
237
	void cmdUdpOpen();
238
	void cmdUdpClose();
239
	void cmdUdpState();
240
	void cmdUdpSend();
241
	void cmdUdpRecv();
242
	void cmdIcmpSend();
243
	void cmdIcmpRecv();
244
	void icmpWorkerLoop();
245
246
	// --- Helpers ---
247
	// The setResult() overloads queue a command reply for the MSX to read
248
	// from the data port (in v2 every reply, success or error, begins with
249
	// the status byte).
250
	void setResult(std::span<const uint8_t> data);
251
	// Wire-layout struct (see UnapiNetWire.hh): the compiler lays out the
252
	// exact on-wire bytes. The requires-clause keeps span-like types (which
253
	// accidentally satisfy wire_layout, being trivially-copyable objects
254
	// holding a pointer) on the span overload above - serializing a span
255
	// OBJECT would emit its pointer bytes, not the pointed-to data.
256
	template<wire_layout T>
257
		requires (!std::convertible_to<const T&, std::span<const uint8_t>>)
258
	void setResult(const T& d)
259
	{
260
		setResult(asBytes(d));
261
	}
262
	// Fixed-size header struct followed by a variable payload
263
	// (used by TCP_RECV / UDP_RECV).
264
	template<wire_layout T>
265
		requires (!std::convertible_to<const T&, std::span<const uint8_t>>)
266
	void setResult(const T& hdr, std::span<const uint8_t> payload)
267
	{
268
		setResult(asBytes(hdr));
269
		resultBuf.insert(resultBuf.end(), payload.begin(), payload.end());
270
	}
271
	// Single-byte reply: {ERR_OK} for the commands whose success carries no
272
	// data, {error code} for every failure (v2 rule 2).
273
	void replyStatus(uint8_t status);
274
275
	// Rule 3 helper: accept the parameter block only at exactly sizeof(T).
276
	// Commands with a variable payload (TCP_SEND / UDP_SEND) check their
277
	// combined size by hand instead.
278
	template<wire_layout T>
279
	[[nodiscard]] std::optional<T> exactParams() const
280
	{
281
		if (paramBuf.size() != sizeof(T)) return std::nullopt;
282
		return fromBytes<T>(paramBuf);
283
	}
284
285
	// Returns a free 0-based index, or INVALID_HANDLE.
286
	[[nodiscard]] int allocTcpHandle();
287
	// Validate a 1-based wire handle and return the connection, or nullptr.
288
	[[nodiscard]] TcpConnection* tcpForHandle(int wireHandle);
289
	// Direct close. Only legal with the socket thread stopped (destructor).
290
	void closeTcp(TcpConnection& c);
291
	// Drop a connection now: the socket is invalidated (so the handle is free
292
	// again immediately) and the raw fd is queued for the socket thread to close.
293
	// Safe from either thread. The buffers and endpoint info are LEFT in
294
	// place - a closed slot keeps its final state, close reason and undrained
295
	// receive data until TCP_OPEN reuses it (the v2 sticky-slot rule; ABORT
296
	// included). clearMetadata wipes them too, which only reset() wants.
297
	void requestClose(TcpConnection& c, CloseReason reason,
298
	                  bool clearMetadata = false);
299
	void requestClose(UdpConnection& u);
300
	// TCP_CLOSE: half-close (FIN) and let the peer finish, rather than dropping
301
	// data the MSX has already been told we accepted. Idempotent while the
302
	// close is in progress.
303
	void gracefulClose(TcpConnection& c);
304
	// Hand a raw fd to the socket thread to close.
305
	void deferSockClose(SOCKET sd);
306
	std::vector<SOCKET> socksToClose; // guarded by closeMutex
307
	std::mutex closeMutex; // lock order: a connection mutex may be held when
308
	                       // taking this one, never the other way round
309
	[[nodiscard]] int allocUdpHandle();
310
	[[nodiscard]] UdpConnection* udpForHandle(int wireHandle);
311
	void closeUdp(UdpConnection& u);
312
	void closeAllConnections();
313
314
};
315
316
} // namespace openmsx
317
318
#endif // UNAPINET_HH