diff --git a/idd/LGIdd/LGIdd.vcxproj b/idd/LGIdd/LGIdd.vcxproj index 2dc8307d..ba8a97a5 100644 --- a/idd/LGIdd/LGIdd.vcxproj +++ b/idd/LGIdd/LGIdd.vcxproj @@ -88,6 +88,7 @@ + @@ -171,6 +172,7 @@ + diff --git a/idd/LGIdd/LGIdd.vcxproj.filters b/idd/LGIdd/LGIdd.vcxproj.filters index ca867ed0..fe3cde3c 100644 --- a/idd/LGIdd/LGIdd.vcxproj.filters +++ b/idd/LGIdd/LGIdd.vcxproj.filters @@ -286,6 +286,9 @@ Transport\LGMP + + Transport\Network + Transport\Network @@ -471,6 +474,9 @@ Transport\LGMP + + Transport\Network + Transport\Network diff --git a/idd/LGIdd/transport/network/CNetworkServer.cpp b/idd/LGIdd/transport/network/CNetworkServer.cpp new file mode 100644 index 00000000..0c0d62f2 --- /dev/null +++ b/idd/LGIdd/transport/network/CNetworkServer.cpp @@ -0,0 +1,907 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include "transport/network/CNetworkServer.h" + +#include "transport/network/CNetworkSession.h" +#include "transport/network/CNetworkTransport.h" +#include "transport/network/NetworkSettings.h" + +#include "CDebug.h" +#include "CSRWLock.h" +#include "NetworkIdentity.h" +#include "common/network/protocol.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + static constexpr uint64_t NETWORK_HANDSHAKE_TIMEOUT_MS = 5000; + static constexpr uint64_t NETWORK_IDLE_TIMEOUT_MS = 30000; + static constexpr uint32_t NETWORK_KEEPALIVE_INTERVAL_MS = 1000; + static constexpr uint32_t NETWORK_INITIAL_RTT_MS = 1; + static constexpr uint16_t NETWORK_PEER_BIDI_STREAMS = 16; + static constexpr uint16_t NETWORK_PEER_UNIDI_STREAMS = 16; + static constexpr size_t NETWORK_MAX_HEADER_SIZE = 4096; + static constexpr size_t NETWORK_MAX_CONTROL_PAYLOAD = 1024 * 1024; + static constexpr size_t NETWORK_MAX_CONTROL_BUFFER = + NETWORK_MAX_HEADER_SIZE + NETWORK_MAX_CONTROL_PAYLOAD; + + static constexpr QUIC_UINT62 NETWORK_ERROR_PROTOCOL = 1; + static constexpr QUIC_UINT62 NETWORK_ERROR_SERVER_STOPPING = 2; + + static const wchar_t NETWORK_REGISTRY_KEY[] = + L"Software\\LookingGlass\\IDD"; + + uint16_t ReadU16(const uint8_t * data) + { + return static_cast(data[0]) | + static_cast(static_cast(data[1]) << 8); + } + + uint32_t ReadU32(const uint8_t * data) + { + return static_cast(data[0]) | + (static_cast(data[1]) << 8) | + (static_cast(data[2]) << 16) | + (static_cast(data[3]) << 24); + } + + bool FillRandom(void * data, size_t size) + { + if (!data || size > ULONG_MAX) + return false; + + return BCryptGenRandom(nullptr, static_cast(data), + static_cast(size), BCRYPT_USE_SYSTEM_PREFERRED_RNG) >= 0; + } + + bool MakeAddress(const std::wstring& source, uint16_t port, + QUIC_ADDR& address) + { + if (source == L"*") + { + ZeroMemory(&address, sizeof(address)); + QuicAddrSetFamily(&address, QUIC_ADDRESS_FAMILY_UNSPEC); + QuicAddrSetPort(&address, port); + return true; + } + + const int length = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, + source.data(), static_cast(source.size()), nullptr, 0, + nullptr, nullptr); + if (length <= 0) + return false; + + std::string text(static_cast(length), '\0'); + if (WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, + source.data(), static_cast(source.size()), &text[0], length, + nullptr, nullptr) != length) + return false; + + ZeroMemory(&address, sizeof(address)); + return QuicAddrFromString(text.c_str(), port, &address) != FALSE; + } + + bool ReadCertificateHash(QUIC_CERTIFICATE_HASH_STORE& certificate) + { + HKEY key = nullptr; + const LSTATUS openStatus = RegOpenKeyExW(HKEY_LOCAL_MACHINE, + NETWORK_REGISTRY_KEY, 0, KEY_QUERY_VALUE | KEY_WOW64_64KEY, &key); + if (openStatus != ERROR_SUCCESS) + { + DEBUG_ERROR("Failed to open the network identity registry key: 0x%lx", + static_cast(openStatus)); + return false; + } + + DWORD type = 0; + DWORD size = sizeof(certificate.ShaHash); + const LSTATUS readStatus = RegQueryValueExW(key, + LG_NETWORK_CERTIFICATE_HASH_VALUE, nullptr, &type, + certificate.ShaHash, &size); + RegCloseKey(key); + if (readStatus != ERROR_SUCCESS || type != REG_BINARY || + size != sizeof(certificate.ShaHash)) + { + DEBUG_ERROR("Failed to read the network identity certificate hash: " + "0x%lx", static_cast(readStatus)); + return false; + } + + certificate.Flags = QUIC_CERTIFICATE_HASH_STORE_FLAG_MACHINE_STORE; + if (strcpy_s(certificate.StoreName, + sizeof(certificate.StoreName), "MY") != 0) + return false; + return true; + } +} + +struct CNetworkServer::Impl +{ + struct Connection; + + struct Send + { + QUIC_BUFFER buffer = {}; + std::vector data; + }; + + enum class HandshakeState + { + WAITING_HELLO, + READY, + }; + + struct Connection + { + explicit Connection(Impl& owner) : owner(owner) {} + + Impl& owner; + CSRWLock lock; + HQUIC handle = nullptr; + HQUIC controlStream = nullptr; + uint32_t client = 0; + uint32_t generation = 0; + uint64_t sessionEpoch = 0; + uint64_t sendSequence = 0; + uint64_t receiveSequence = 0; + uint32_t maxPayload = + static_cast(NETWORK_MAX_CONTROL_PAYLOAD); + bool closing = false; + HandshakeState handshake = + HandshakeState::WAITING_HELLO; + std::shared_ptr session; + std::vector receiveData; + }; + + struct CallbackGuard + { + explicit CallbackGuard(Impl& owner) : owner(owner) + { + owner.EnterCallback(); + } + + ~CallbackGuard() + { + owner.LeaveCallback(); + } + + Impl& owner; + }; + + Impl(CNetworkTransport& transport, const NetworkSettings& settings) : + transport(transport), settings(settings) + { + } + + ~Impl() + { + Stop(); + if (drainedEvent) + CloseHandle(drainedEvent); + } + + CNetworkTransport& transport; + NetworkSettings settings; + const QUIC_API_TABLE * api = nullptr; + HQUIC registration = nullptr; + HQUIC configuration = nullptr; + HQUIC listener = nullptr; + HANDLE drainedEvent = nullptr; + CSRWLock lock; + std::vector connections; + uint32_t nextClient = 1; + unsigned activeCallbacks = 0; + bool stopping = true; + bool running = false; + + void EnterCallback() + { + CSRWExclusiveLock guard(lock); + ++activeCallbacks; + } + + void LeaveCallback() + { + bool drained = false; + { + CSRWExclusiveLock guard(lock); + if (activeCallbacks) + --activeCallbacks; + drained = stopping && connections.empty() && !activeCallbacks; + } + if (drained && drainedEvent) + SetEvent(drainedEvent); + } + + uint32_t AllocateClientLocked() + { + for (uint64_t attempt = 0; attempt < UINT32_MAX; ++attempt) + { + const uint32_t candidate = nextClient++; + if (!nextClient) + nextClient = 1; + if (!candidate) + continue; + + bool found = false; + for (const Connection * connection : connections) + if (connection->client == candidate) + { + found = true; + break; + } + if (!found) + return candidate; + } + return 0; + } + + void Shutdown(Connection& connection, QUIC_UINT62 error) + { + HQUIC handle = nullptr; + { + CSRWExclusiveLock guard(connection.lock); + if (connection.closing) + return; + connection.closing = true; + handle = connection.handle; + } + if (handle) + api->ConnectionShutdown(handle, + QUIC_CONNECTION_SHUTDOWN_FLAG_NONE, error); + } + + bool SendPacket(Connection& connection, LGNetService service, + uint16_t messageType, LGNetEnvelopeFlags flags, uint64_t requestID, + const void * payload, uint32_t payloadLength) + { + if ((payloadLength && !payload) || + payloadLength > connection.maxPayload) + return false; + + std::unique_ptr send(new (std::nothrow) Send()); + if (!send) + return false; + + const size_t packetSize = LG_NET_ENVELOPE_WIRE_SIZE + payloadLength; + try + { + send->data.resize(packetSize); + } + catch (...) + { + return false; + } + + LGNetEnvelope envelope; + lgNetEnvelopeInit(&envelope, service, messageType, + LG_NET_CORE_VERSION_CURRENT, LG_NET_MESSAGE_VERSION_CURRENT); + envelope.flags = flags | LG_NET_ENVELOPE_RELIABLE; + envelope.payloadLength = payloadLength; + envelope.requestID = requestID; + + send->buffer.Buffer = send->data.data(); + send->buffer.Length = static_cast(send->data.size()); + QUIC_STATUS status; + { + CSRWExclusiveLock guard(connection.lock); + if (connection.closing || !connection.controlStream) + return false; + if (connection.sendSequence == UINT64_MAX) + return false; + envelope.sessionEpoch = connection.sessionEpoch; + envelope.sequence = ++connection.sendSequence; + if (!lgNetPacketEncode(send->data.data(), send->data.size(), + &envelope, payload)) + return false; + status = api->StreamSend(connection.controlStream, &send->buffer, 1, + QUIC_SEND_FLAG_NONE, send.get()); + } + if (QUIC_FAILED(status)) + return false; + send.release(); + return true; + } + + bool HandleHello(Connection& connection, const LGNetPacketView& packet) + { + if (packet.envelope.requestID == 0 || + packet.envelope.sessionEpoch != 0 || + packet.envelope.componentEpoch != 0) + return false; + + LGNetHello hello; + if (lgNetHelloDecode(&hello, packet.payload, + packet.payloadLength) != LG_NET_PARSE_OK || + hello.role != LG_NET_ROLE_CLIENT || hello.capabilityCount != 0 || + hello.protocolMin > LG_NET_PROTOCOL_VERSION_CURRENT || + hello.protocolMax < LG_NET_PROTOCOL_VERSION_CURRENT || + (hello.authModes & ~LG_NET_AUTH_MODES_KNOWN) != 0 || + !(hello.authModes & LG_NET_AUTH_MODE_FLAG(LG_NET_AUTH_NONE))) + return false; + + uint64_t sessionEpoch = 0; + uint8_t nonce[LG_NET_HELLO_NONCE_LENGTH]; + if (!FillRandom(&sessionEpoch, sizeof(sessionEpoch)) || + !FillRandom(nonce, sizeof(nonce))) + return false; + if (!sessionEpoch) + sessionEpoch = GetTickCount64() | 1; + + std::shared_ptr session = transport.CreateSession( + connection.client, connection.generation); + if (!session) + return false; + + { + CSRWExclusiveLock guard(connection.lock); + if (connection.closing || + connection.handshake != HandshakeState::WAITING_HELLO) + { + transport.RemoveSession(connection.client, connection.generation); + return false; + } + connection.session = std::move(session); + connection.sessionEpoch = sessionEpoch; + connection.maxPayload = hello.maxPayloadLength < + NETWORK_MAX_CONTROL_PAYLOAD ? hello.maxPayloadLength : + static_cast(NETWORK_MAX_CONTROL_PAYLOAD); + } + + LGNetHelloAck acknowledgement; + lgNetHelloAckInit(&acknowledgement, sessionEpoch); + acknowledgement.authMode = LG_NET_AUTH_NONE; + acknowledgement.maxPayloadLength = connection.maxPayload; + acknowledgement.maxDatagramPayload = 0; + acknowledgement.capabilityCount = 0; + memcpy(acknowledgement.nonce, nonce, sizeof(nonce)); + + uint8_t payload[LG_NET_HELLO_ACK_WIRE_SIZE]; + if (!lgNetHelloAckEncode(payload, sizeof(payload), &acknowledgement) || + !SendPacket(connection, LG_NET_SERVICE_CORE, + LG_NET_CORE_MESSAGE_HELLO_ACK, + LG_NET_ENVELOPE_RESPONSE | LG_NET_ENVELOPE_FINAL, + packet.envelope.requestID, payload, sizeof(payload)) || + !SendPacket(connection, LG_NET_SERVICE_CORE, + LG_NET_CORE_MESSAGE_CAPABILITIES_DONE, + LG_NET_ENVELOPE_RESPONSE | LG_NET_ENVELOPE_FINAL, + packet.envelope.requestID, nullptr, 0)) + return false; + + { + CSRWExclusiveLock guard(connection.lock); + if (connection.closing) + return false; + connection.handshake = HandshakeState::READY; + } + DEBUG_INFO("Network client %u completed the control handshake", + connection.client); + return true; + } + + bool HandlePacket(Connection& connection, const LGNetPacketView& packet) + { + if (packet.envelope.headerSize > NETWORK_MAX_HEADER_SIZE || + packet.envelope.payloadLength > NETWORK_MAX_CONTROL_PAYLOAD || + packet.envelope.messageVersion != LG_NET_MESSAGE_VERSION_CURRENT || + packet.envelope.flags & (LG_NET_ENVELOPE_RESPONSE | + LG_NET_ENVELOPE_DATAGRAM | LG_NET_ENVELOPE_ERROR | + LG_NET_ENVELOPE_EARLY_DATA) || + !(packet.envelope.flags & LG_NET_ENVELOPE_RELIABLE) || + !(packet.envelope.flags & LG_NET_ENVELOPE_FINAL) || + !lgNetServiceKnown(packet.envelope.service) || + !lgNetMessageKnown(packet.envelope.service, + packet.envelope.messageType)) + return false; + + HandshakeState handshake; + uint64_t sessionEpoch; + { + CSRWSharedLock guard(connection.lock); + handshake = connection.handshake; + sessionEpoch = connection.sessionEpoch; + } + + if (handshake == HandshakeState::WAITING_HELLO) + return packet.envelope.service == LG_NET_SERVICE_CORE && + packet.envelope.messageType == LG_NET_CORE_MESSAGE_HELLO && + packet.envelope.serviceVersion == LG_NET_CORE_VERSION_CURRENT && + HandleHello(connection, packet); + + if (packet.envelope.sessionEpoch != sessionEpoch || + packet.envelope.service != LG_NET_SERVICE_CORE || + packet.envelope.serviceVersion != LG_NET_CORE_VERSION_CURRENT) + return false; + + if (packet.envelope.messageType == LG_NET_CORE_MESSAGE_PING) + return SendPacket(connection, LG_NET_SERVICE_CORE, + LG_NET_CORE_MESSAGE_PONG, + LG_NET_ENVELOPE_RESPONSE | LG_NET_ENVELOPE_FINAL, + packet.envelope.requestID, packet.payload, + static_cast(packet.payloadLength)); + + if (packet.envelope.messageType == LG_NET_CORE_MESSAGE_GOODBYE && + packet.payloadLength == 0) + { + Shutdown(connection, 0); + return true; + } + return false; + } + + bool Receive(Connection& connection, const QUIC_BUFFER * buffers, + uint32_t count) + { + size_t additional = 0; + for (uint32_t i = 0; i < count; ++i) + { + if (buffers[i].Length > NETWORK_MAX_CONTROL_BUFFER - additional) + return false; + additional += buffers[i].Length; + } + if (connection.receiveData.size() > + NETWORK_MAX_CONTROL_BUFFER - additional) + return false; + + try + { + for (uint32_t i = 0; i < count; ++i) + connection.receiveData.insert(connection.receiveData.end(), + buffers[i].Buffer, buffers[i].Buffer + buffers[i].Length); + } + catch (...) + { + return false; + } + + while (connection.receiveData.size() >= LG_NET_ENVELOPE_WIRE_SIZE) + { + const uint16_t headerSize = ReadU16(connection.receiveData.data() + 8); + const uint32_t payloadSize = ReadU32(connection.receiveData.data() + 24); + if (headerSize < LG_NET_ENVELOPE_WIRE_SIZE || + headerSize > NETWORK_MAX_HEADER_SIZE || + payloadSize > NETWORK_MAX_CONTROL_PAYLOAD || + payloadSize > NETWORK_MAX_CONTROL_BUFFER - headerSize) + return false; + + const size_t packetSize = static_cast(headerSize) + payloadSize; + if (connection.receiveData.size() < packetSize) + break; + + LGNetPacketView packet; + if (lgNetPacketDecode(&packet, connection.receiveData.data(), + packetSize) != LG_NET_PARSE_OK || packet.wireSize != packetSize) + return false; + + { + CSRWExclusiveLock guard(connection.lock); + if (!packet.envelope.sequence || + connection.receiveSequence == UINT64_MAX || + packet.envelope.sequence != connection.receiveSequence + 1) + return false; + connection.receiveSequence = packet.envelope.sequence; + } + if (!HandlePacket(connection, packet)) + return false; + + connection.receiveData.erase(connection.receiveData.begin(), + connection.receiveData.begin() + packetSize); + } + return true; + } + + void ConnectionClosed(Connection& connection) + { + const uint32_t client = connection.client; + const uint32_t generation = connection.generation; + { + CSRWExclusiveLock guard(lock); + const auto found = std::find( + connections.begin(), connections.end(), &connection); + if (found != connections.end()) + connections.erase(found); + } + + if (connection.session) + transport.RemoveSession(client, generation); + connection.session.reset(); + delete &connection; + DEBUG_INFO("Network client %u disconnected", client); + } + + static QUIC_STATUS QUIC_API ListenerCallback(HQUIC handle, + void * context, QUIC_LISTENER_EVENT * event) + { + UNREFERENCED_PARAMETER(handle); + Impl& owner = *static_cast(context); + CallbackGuard callback(owner); + if (event->Type != QUIC_LISTENER_EVENT_NEW_CONNECTION) + return QUIC_STATUS_SUCCESS; + + Connection * connection = nullptr; + { + CSRWExclusiveLock guard(owner.lock); + if (owner.stopping || + owner.connections.size() >= owner.settings.maxClients) + return QUIC_STATUS_CONNECTION_REFUSED; + + connection = new (std::nothrow) Connection(owner); + if (!connection) + return QUIC_STATUS_OUT_OF_MEMORY; + connection->handle = event->NEW_CONNECTION.Connection; + connection->client = owner.AllocateClientLocked(); + connection->generation = 1; + if (!connection->client) + { + delete connection; + return QUIC_STATUS_CONNECTION_REFUSED; + } + try + { + owner.connections.push_back(connection); + } + catch (...) + { + delete connection; + return QUIC_STATUS_OUT_OF_MEMORY; + } + ResetEvent(owner.drainedEvent); + } + + owner.api->SetCallbackHandler(event->NEW_CONNECTION.Connection, + reinterpret_cast(ConnectionCallback), connection); + const QUIC_STATUS status = owner.api->ConnectionSetConfiguration( + event->NEW_CONNECTION.Connection, owner.configuration); + if (QUIC_FAILED(status)) + { + { + CSRWExclusiveLock guard(owner.lock); + const auto found = std::find(owner.connections.begin(), + owner.connections.end(), connection); + if (found != owner.connections.end()) + owner.connections.erase(found); + } + delete connection; + } + return status; + } + + static QUIC_STATUS QUIC_API ConnectionCallback(HQUIC handle, + void * context, QUIC_CONNECTION_EVENT * event) + { + Connection& connection = *static_cast(context); + Impl& owner = connection.owner; + CallbackGuard callback(owner); + + switch (event->Type) + { + case QUIC_CONNECTION_EVENT_CONNECTED: + DEBUG_INFO("Network client %u connected", connection.client); + return QUIC_STATUS_SUCCESS; + + case QUIC_CONNECTION_EVENT_PEER_STREAM_STARTED: + { + const bool invalidFlags = + event->PEER_STREAM_STARTED.Flags & + (QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL | + QUIC_STREAM_OPEN_FLAG_0_RTT); + bool accepted = false; + { + CSRWExclusiveLock guard(connection.lock); + if (!invalidFlags && !connection.closing && + !connection.controlStream) + { + connection.controlStream = event->PEER_STREAM_STARTED.Stream; + accepted = true; + } + } + + if (accepted) + owner.api->SetCallbackHandler(event->PEER_STREAM_STARTED.Stream, + reinterpret_cast(StreamCallback), &connection); + else + { + owner.api->SetCallbackHandler(event->PEER_STREAM_STARTED.Stream, + reinterpret_cast(RejectedStreamCallback), &owner); + owner.api->StreamShutdown(event->PEER_STREAM_STARTED.Stream, + QUIC_STREAM_SHUTDOWN_FLAG_ABORT, NETWORK_ERROR_PROTOCOL); + owner.Shutdown(connection, NETWORK_ERROR_PROTOCOL); + } + return QUIC_STATUS_SUCCESS; + } + + case QUIC_CONNECTION_EVENT_DATAGRAM_RECEIVED: + owner.Shutdown(connection, NETWORK_ERROR_PROTOCOL); + return QUIC_STATUS_SUCCESS; + + case QUIC_CONNECTION_EVENT_SHUTDOWN_INITIATED_BY_TRANSPORT: + case QUIC_CONNECTION_EVENT_SHUTDOWN_INITIATED_BY_PEER: + { + CSRWExclusiveLock guard(connection.lock); + connection.closing = true; + return QUIC_STATUS_SUCCESS; + } + + case QUIC_CONNECTION_EVENT_SHUTDOWN_COMPLETE: + owner.api->ConnectionClose(handle); + owner.ConnectionClosed(connection); + return QUIC_STATUS_SUCCESS; + + default: + return QUIC_STATUS_SUCCESS; + } + } + + static QUIC_STATUS QUIC_API StreamCallback(HQUIC handle, + void * context, QUIC_STREAM_EVENT * event) + { + Connection& connection = *static_cast(context); + Impl& owner = connection.owner; + CallbackGuard callback(owner); + + switch (event->Type) + { + case QUIC_STREAM_EVENT_RECEIVE: + if (!owner.Receive(connection, event->RECEIVE.Buffers, + event->RECEIVE.BufferCount)) + owner.Shutdown(connection, NETWORK_ERROR_PROTOCOL); + return QUIC_STATUS_SUCCESS; + + case QUIC_STREAM_EVENT_SEND_COMPLETE: + delete static_cast(event->SEND_COMPLETE.ClientContext); + return QUIC_STATUS_SUCCESS; + + case QUIC_STREAM_EVENT_PEER_SEND_SHUTDOWN: + case QUIC_STREAM_EVENT_PEER_SEND_ABORTED: + case QUIC_STREAM_EVENT_PEER_RECEIVE_ABORTED: + owner.Shutdown(connection, NETWORK_ERROR_PROTOCOL); + return QUIC_STATUS_SUCCESS; + + case QUIC_STREAM_EVENT_SHUTDOWN_COMPLETE: + { + { + CSRWExclusiveLock guard(connection.lock); + if (connection.controlStream == handle) + connection.controlStream = nullptr; + } + owner.api->StreamClose(handle); + return QUIC_STATUS_SUCCESS; + } + + default: + return QUIC_STATUS_SUCCESS; + } + } + + static QUIC_STATUS QUIC_API RejectedStreamCallback(HQUIC handle, + void * context, QUIC_STREAM_EVENT * event) + { + Impl& owner = *static_cast(context); + CallbackGuard callback(owner); + if (event->Type == QUIC_STREAM_EVENT_SHUTDOWN_COMPLETE) + owner.api->StreamClose(handle); + return QUIC_STATUS_SUCCESS; + } + + bool Start() + { + { + CSRWExclusiveLock guard(lock); + if (running) + return true; + stopping = false; + } + + if (settings.auth != LG_NET_AUTH_NONE) + { + DEBUG_ERROR("Network password authentication is configured but the " + "password proof provider is not implemented; refusing to listen"); + Stop(); + return false; + } + + if (!drainedEvent) + drainedEvent = CreateEventW(nullptr, TRUE, TRUE, nullptr); + if (!drainedEvent || QUIC_FAILED(MsQuicOpen2(&api))) + { + DEBUG_ERROR("Failed to initialize the static MsQuic API"); + Stop(); + return false; + } + + QUIC_REGISTRATION_CONFIG registrationConfig = {}; + registrationConfig.AppName = "looking-glass-idd"; + registrationConfig.ExecutionProfile = QUIC_EXECUTION_PROFILE_LOW_LATENCY; + QUIC_STATUS status = api->RegistrationOpen( + ®istrationConfig, ®istration); + if (QUIC_FAILED(status)) + { + DEBUG_ERROR("MsQuic RegistrationOpen failed: 0x%lx", + static_cast(status)); + Stop(); + return false; + } + + QUIC_SETTINGS quicSettings = {}; + quicSettings.IsSet.HandshakeIdleTimeoutMs = TRUE; + quicSettings.IsSet.IdleTimeoutMs = TRUE; + quicSettings.IsSet.InitialRttMs = TRUE; + quicSettings.IsSet.MaxAckDelayMs = TRUE; + quicSettings.IsSet.KeepAliveIntervalMs = TRUE; + quicSettings.IsSet.PeerBidiStreamCount = TRUE; + quicSettings.IsSet.PeerUnidiStreamCount = TRUE; + quicSettings.IsSet.DatagramReceiveEnabled = TRUE; + quicSettings.IsSet.ServerResumptionLevel = TRUE; + quicSettings.HandshakeIdleTimeoutMs = NETWORK_HANDSHAKE_TIMEOUT_MS; + quicSettings.IdleTimeoutMs = NETWORK_IDLE_TIMEOUT_MS; + quicSettings.InitialRttMs = NETWORK_INITIAL_RTT_MS; + quicSettings.MaxAckDelayMs = 1; + quicSettings.KeepAliveIntervalMs = + NETWORK_KEEPALIVE_INTERVAL_MS; + quicSettings.PeerBidiStreamCount = NETWORK_PEER_BIDI_STREAMS; + quicSettings.PeerUnidiStreamCount = NETWORK_PEER_UNIDI_STREAMS; + quicSettings.DatagramReceiveEnabled = TRUE; + quicSettings.ServerResumptionLevel = QUIC_SERVER_NO_RESUME; + + QUIC_BUFFER alpn = {}; + alpn.Buffer = reinterpret_cast( + const_cast(LG_NET_ALPN)); + alpn.Length = static_cast(LG_NET_ALPN_LENGTH); + status = api->ConfigurationOpen(registration, &alpn, 1, + &quicSettings, sizeof(quicSettings), nullptr, &configuration); + if (QUIC_FAILED(status)) + { + DEBUG_ERROR("MsQuic ConfigurationOpen failed: 0x%lx", + static_cast(status)); + Stop(); + return false; + } + + QUIC_CERTIFICATE_HASH_STORE certificate = {}; + if (!ReadCertificateHash(certificate)) + { + DEBUG_ERROR("The persistent network identity is unavailable; run the " + "Looking Glass IDD installer to provision it"); + Stop(); + return false; + } + + QUIC_CREDENTIAL_CONFIG credential = {}; + credential.Type = + QUIC_CREDENTIAL_TYPE_CERTIFICATE_HASH_STORE; + credential.Flags = QUIC_CREDENTIAL_FLAG_NONE; + credential.CertificateHashStore = &certificate; + status = api->ConfigurationLoadCredential(configuration, &credential); + if (QUIC_FAILED(status)) + { + DEBUG_ERROR("MsQuic failed to load the network identity: 0x%lx", + static_cast(status)); + Stop(); + return false; + } + + status = api->ListenerOpen( + registration, ListenerCallback, this, &listener); + if (QUIC_FAILED(status)) + { + DEBUG_ERROR("MsQuic ListenerOpen failed: 0x%lx", + static_cast(status)); + Stop(); + return false; + } + + QUIC_ADDR address = {}; + if (!MakeAddress(settings.bindAddress, settings.port, address)) + { + DEBUG_ERROR("Invalid network bind address: %ls", + settings.bindAddress.c_str()); + Stop(); + return false; + } + status = api->ListenerStart(listener, &alpn, 1, &address); + if (QUIC_FAILED(status)) + { + DEBUG_ERROR("MsQuic ListenerStart failed on [%ls]:%u: 0x%lx", + settings.bindAddress.c_str(), + static_cast(settings.port), + static_cast(status)); + Stop(); + return false; + } + + { + CSRWExclusiveLock guard(lock); + running = true; + } + DEBUG_INFO("Network transport listening on [%ls]:%u", + settings.bindAddress.c_str(), static_cast(settings.port)); + return true; + } + + void Stop() + { + { + CSRWExclusiveLock guard(lock); + stopping = true; + running = false; + if (connections.empty() && !activeCallbacks && drainedEvent) + SetEvent(drainedEvent); + } + + if (listener && api) + { + api->ListenerClose(listener); + listener = nullptr; + } + if (registration && api) + api->RegistrationShutdown(registration, + QUIC_CONNECTION_SHUTDOWN_FLAG_SILENT, + NETWORK_ERROR_SERVER_STOPPING); + if (drainedEvent) + WaitForSingleObject(drainedEvent, INFINITE); + + if (configuration && api) + { + api->ConfigurationClose(configuration); + configuration = nullptr; + } + if (registration && api) + { + api->RegistrationClose(registration); + registration = nullptr; + } + if (api) + { + MsQuicClose(api); + api = nullptr; + } + } +}; + +CNetworkServer::CNetworkServer(CNetworkTransport& transport, + const NetworkSettings& settings) : + m_impl(new Impl(transport, settings)) +{ +} + +CNetworkServer::~CNetworkServer() = default; + +bool CNetworkServer::Start() +{ + return m_impl && m_impl->Start(); +} + +void CNetworkServer::Stop() +{ + if (m_impl) + m_impl->Stop(); +} diff --git a/idd/LGIdd/transport/network/CNetworkServer.h b/idd/LGIdd/transport/network/CNetworkServer.h new file mode 100644 index 00000000..70036f39 --- /dev/null +++ b/idd/LGIdd/transport/network/CNetworkServer.h @@ -0,0 +1,44 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#pragma once + +#include + +class CNetworkTransport; +struct NetworkSettings; + +class CNetworkServer final +{ +private: + struct Impl; + std::unique_ptr m_impl; + +public: + CNetworkServer(CNetworkTransport& transport, + const NetworkSettings& settings); + ~CNetworkServer(); + + CNetworkServer(const CNetworkServer&) = delete; + CNetworkServer& operator=(const CNetworkServer&) = delete; + + bool Start(); + void Stop(); +}; diff --git a/idd/LGIdd/transport/network/CNetworkTransport.cpp b/idd/LGIdd/transport/network/CNetworkTransport.cpp index 6682073c..48880650 100644 --- a/idd/LGIdd/transport/network/CNetworkTransport.cpp +++ b/idd/LGIdd/transport/network/CNetworkTransport.cpp @@ -20,6 +20,8 @@ #include "transport/network/CNetworkTransport.h" +#include "transport/network/CNetworkServer.h" + #include "CDebug.h" #include @@ -72,6 +74,16 @@ bool CNetworkTransport::Initialize() if (m_state != State::OPEN) return false; + try + { + m_server.reset(new CNetworkServer(*this, m_settings)); + } + catch (...) + { + DEBUG_ERROR("Failed to allocate the network server"); + return false; + } + m_state = State::INITIALIZED; if (m_config.services & TRANSPORT_SERVICE_FRAME) DEBUG_WARN("Network video is unavailable until the GPU codec and " @@ -81,15 +93,29 @@ bool CNetworkTransport::Initialize() bool CNetworkTransport::Setup(size_t alignment) { + CNetworkServer * server; + { + CSRWExclusiveLock lock(m_lifecycleLock); + if (m_state == State::READY) + return m_alignment == alignment; + if (m_state != State::INITIALIZED) + return false; + + m_alignment = alignment; + m_state = State::READY; + server = m_server.get(); + } + + if (server && server->Start()) + return true; + CSRWExclusiveLock lock(m_lifecycleLock); if (m_state == State::READY) - return m_alignment == alignment; - if (m_state != State::INITIALIZED) - return false; - - m_alignment = alignment; - m_state = State::READY; - return true; + { + m_alignment = 0; + m_state = State::INITIALIZED; + } + return false; } ITransport::ProcessResult CNetworkTransport::Process( @@ -115,13 +141,18 @@ ITransport::ProcessResult CNetworkTransport::Process( void CNetworkTransport::Stop() { + CNetworkServer * server; { CSRWExclusiveLock lock(m_lifecycleLock); if (m_state == State::STOPPED) return; m_state = State::STOPPED; + server = m_server.get(); } + if (server) + server->Stop(); + std::vector sessions; { CSRWExclusiveLock lock(m_sessionLock); diff --git a/idd/LGIdd/transport/network/CNetworkTransport.h b/idd/LGIdd/transport/network/CNetworkTransport.h index e52efc20..16dd12c5 100644 --- a/idd/LGIdd/transport/network/CNetworkTransport.h +++ b/idd/LGIdd/transport/network/CNetworkTransport.h @@ -28,6 +28,8 @@ #include #include +class CNetworkServer; + // This provider owns transport-independent lifecycle and session state. It // deliberately advertises no frame capabilities and no texture sink until a // GPU codec backend is attached; it must not be mistaken for working media. @@ -45,8 +47,9 @@ private: STOPPED, }; - TransportInstance m_config; - NetworkSettings m_settings; + TransportInstance m_config; + NetworkSettings m_settings; + std::unique_ptr m_server; mutable CSRWLock m_lifecycleLock; State m_state = State::CONSTRUCTED;