diff --git a/client/transports/CMakeLists.txt b/client/transports/CMakeLists.txt index 27829986..bbd80665 100644 --- a/client/transports/CMakeLists.txt +++ b/client/transports/CMakeLists.txt @@ -20,6 +20,9 @@ endfunction() # Add/remove transports here! add_transport(LGMP) +if(ENABLE_NETWORK) + add_transport(Network) +endif() if(ENABLE_SPICE) add_transport(SPICE) endif() diff --git a/client/transports/Network/CMakeLists.txt b/client/transports/Network/CMakeLists.txt new file mode 100644 index 00000000..42cd8530 --- /dev/null +++ b/client/transports/Network/CMakeLists.txt @@ -0,0 +1,47 @@ +cmake_minimum_required(VERSION 3.10) +project(transport_Network LANGUAGES C) + +if(NOT TARGET msquic::msquic) + find_package(msquic CONFIG REQUIRED) +endif() + +set(TRANSPORT_NETWORK_SOURCES + connection.c + known_hosts.c + network.c +) + +if(WIN32) + list(APPEND TRANSPORT_NETWORK_SOURCES + platform/windows/fingerprint.c + platform/windows/platform.c + ) +else() + find_package(OpenSSL REQUIRED COMPONENTS Crypto) + list(APPEND TRANSPORT_NETWORK_SOURCES + platform/linux/fingerprint.c + platform/linux/platform.c + ) +endif() + +add_library(transport_Network STATIC ${TRANSPORT_NETWORK_SOURCES}) + +target_include_directories(transport_Network PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/../../src +) + +target_link_libraries(transport_Network PRIVATE + lg_common + msquic::msquic +) + +if(WIN32) + target_link_libraries(transport_Network PRIVATE + bcrypt + crypt32 + ) +else() + target_link_libraries(transport_Network PRIVATE + OpenSSL::Crypto + ) +endif() diff --git a/client/transports/Network/connection.c b/client/transports/Network/connection.c new file mode 100644 index 00000000..a0714f1b --- /dev/null +++ b/client/transports/Network/connection.c @@ -0,0 +1,1203 @@ +/** + * 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 "connection.h" +#include "fingerprint.h" + +#include "common/debug.h" +#include "common/event.h" +#include "common/locking.h" + +#include + +#if !defined(QUIC_API_VERSION_2) +#error "The network transport requires MsQuic API version 2" +#endif + +#include +#include +#include +#include + +#define NETWORK_CONNECT_POLL_MS 50U +#define NETWORK_HANDSHAKE_TIMEOUT_MS 5000U +#define NETWORK_IDLE_TIMEOUT_MS 10000U +#define NETWORK_KEEPALIVE_INTERVAL_MS 1000U +#define NETWORK_INITIAL_RTT_MS 1U +#define NETWORK_MAX_PEER_STREAMS 16U +#define NETWORK_MAX_HEADER_SIZE 4096U +#define NETWORK_MAX_CONTROL_PAYLOAD (1024U * 1024U) +#define NETWORK_MAX_CONTROL_BUFFER \ + (2U * (NETWORK_MAX_HEADER_SIZE + NETWORK_MAX_CONTROL_PAYLOAD)) +#define NETWORK_MAX_DATAGRAM_PAYLOAD 1136U +#define NETWORK_HELLO_REQUEST_ID UINT64_C(1) + +struct NetworkSend +{ + QUIC_BUFFER buffer; + uint8_t data[]; +}; + +struct NetworkConnection +{ + const QUIC_API_TABLE * api; + HQUIC registration; + HQUIC configuration; + HQUIC connection; + HQUIC controlStream; + + LGEvent * connectEvent; + LGEvent * closeEvent; + LG_Lock lock; + LG_Lock stateCallbackLock; + + atomic_bool stopping; + atomic_bool sessionValid; + + char * address; + uint16_t port; + char * password; + char * endpoint; + + NetworkKnownHosts * knownHosts; + NetworkTrustPromptFn trustPrompt; + NetworkTrustCancelFn trustCancel; + NetworkPacketFn packet; + NetworkConnectionStateFn state; + void * opaque; + + bool certificatePending; + char pendingFingerprint[NETWORK_FINGERPRINT_TEXT_SIZE]; + + LG_TransportStatus connectStatus; + bool connecting; + LG_TransportSession session; + uint64_t sessionEpoch; + uint64_t sendSequence; + uint64_t receiveSequence; + bool helloAcknowledged; + bool authenticationComplete; + bool capabilitiesComplete; + bool connectFailed; + uint32_t expectedCapabilities; + uint32_t receivedCapabilities; + uint16_t receivedServices; + uint16_t reliableServices; + uint32_t maxPayloadLength; + uint32_t maxDatagramPayload; + uint32_t serviceMaxPayload[LG_NET_SERVICE_USB + 1U]; + uint32_t serviceMaxDatagram[LG_NET_SERVICE_USB + 1U]; + + uint8_t * receiveData; + size_t receiveSize; + size_t receiveCapacity; +}; + +static QUIC_STATUS QUIC_API networkConnectionCallback(HQUIC handle, + void * opaque, QUIC_CONNECTION_EVENT * event); +static QUIC_STATUS QUIC_API networkStreamCallback(HQUIC handle, + void * opaque, QUIC_STREAM_EVENT * event); +static QUIC_STATUS QUIC_API networkRejectedStreamCallback(HQUIC handle, + void * opaque, QUIC_STREAM_EVENT * event); + +static char * networkDuplicate(const char * value) +{ + if (!value) + return NULL; + + const size_t length = strlen(value) + 1U; + char * result = malloc(length); + if (result) + memcpy(result, value, length); + return result; +} + +static char * networkEndpoint(const char * address, uint16_t port) +{ + const size_t length = strlen(address) + 10U; + char * endpoint = malloc(length); + if (!endpoint) + return NULL; + + snprintf(endpoint, length, "[%s]:%u", address, port); + return endpoint; +} + +static uint16_t networkServiceVersion(LGNetService service) +{ + switch (service) + { + case LG_NET_SERVICE_CORE: + return LG_NET_CORE_VERSION_CURRENT; + + case LG_NET_SERVICE_RECOVERY: + return LG_NET_RECOVERY_VERSION_CURRENT; + + case LG_NET_SERVICE_VIDEO: + return LG_NET_VIDEO_VERSION_CURRENT; + + case LG_NET_SERVICE_CURSOR: + return LG_NET_CURSOR_VERSION_CURRENT; + + case LG_NET_SERVICE_INPUT: + return LG_NET_INPUT_VERSION_CURRENT; + + case LG_NET_SERVICE_AUDIO: + return LG_NET_AUDIO_VERSION_CURRENT; + + case LG_NET_SERVICE_CLIPBOARD: + return LG_NET_CLIPBOARD_VERSION_CURRENT; + + case LG_NET_SERVICE_FILE: + return LG_NET_FILE_VERSION_CURRENT; + + case LG_NET_SERVICE_USB: + return LG_NET_USB_VERSION_RESERVED; + } + + return 0; +} + +static uint16_t networkReadU16(const uint8_t * data) +{ + return (uint16_t)data[0] | (uint16_t)((uint16_t)data[1] << 8); +} + +static uint32_t networkReadU32(const uint8_t * data) +{ + return (uint32_t)data[0] | + ((uint32_t)data[1] << 8) | + ((uint32_t)data[2] << 16) | + ((uint32_t)data[3] << 24); +} + +static void networkConnectionSetState(NetworkConnection * this, + bool connected, LG_TransportStatus reason) +{ + LG_LOCK(this->stateCallbackLock); + atomic_store_explicit( + &this->sessionValid, connected, memory_order_release); + if (this->state) + this->state(this->opaque, connected, reason); + LG_UNLOCK(this->stateCallbackLock); +} + +static void networkConnectionFail(NetworkConnection * this, + LG_TransportStatus status) +{ + const bool wasStopping = atomic_exchange_explicit( + &this->stopping, true, memory_order_acq_rel); + LG_TransportStatus reason; + + LG_LOCK(this->lock); + if (this->connecting && !this->connectFailed) + { + this->connectStatus = wasStopping ? LG_TRANSPORT_DISCONNECTED : status; + this->connectFailed = true; + } + reason = this->connectStatus == LG_TRANSPORT_OK ? + status : this->connectStatus; + LG_UNLOCK(this->lock); + + networkConnectionSetState(this, false, reason); + lgSignalEvent(this->connectEvent); +} + +static bool networkConnectionSendPacket(NetworkConnection * this, + LGNetService service, uint16_t messageType, uint16_t serviceVersion, + LGNetEnvelopeFlags flags, uint64_t componentEpoch, uint64_t requestID, + const void * payload, uint32_t payloadLength, bool allowConnecting) +{ + if (payloadLength > NETWORK_MAX_CONTROL_PAYLOAD || + (payloadLength && !payload) || + !lgNetServiceKnown(service) || + !lgNetMessageKnown(service, messageType) || + serviceVersion != networkServiceVersion(service) || + (flags & + (LG_NET_ENVELOPE_DATAGRAM | LG_NET_ENVELOPE_EARLY_DATA)) || + atomic_load_explicit(&this->stopping, memory_order_acquire)) + return false; + + const size_t packetSize = LG_NET_ENVELOPE_WIRE_SIZE + + payloadLength; + struct NetworkSend * send = malloc(sizeof(*send) + packetSize); + if (!send) + return false; + + LGNetEnvelope envelope; + lgNetEnvelopeInit(&envelope, service, messageType, serviceVersion, + LG_NET_MESSAGE_VERSION_CURRENT); + envelope.flags = flags | LG_NET_ENVELOPE_RELIABLE; + envelope.payloadLength = payloadLength; + envelope.componentEpoch = componentEpoch; + envelope.requestID = requestID; + + LG_LOCK(this->lock); + HQUIC stream = this->controlStream; + const bool serviceReady = + allowConnecting || service == LG_NET_SERVICE_CORE || + ((this->receivedServices & (UINT16_C(1) << service)) && + (this->reliableServices & (UINT16_C(1) << service))); + const uint32_t maxPayloadLength = allowConnecting ? + NETWORK_MAX_CONTROL_PAYLOAD : + service == LG_NET_SERVICE_CORE ? + this->maxPayloadLength : this->serviceMaxPayload[service]; + if (!stream || (!allowConnecting && + !atomic_load_explicit(&this->sessionValid, memory_order_acquire)) || + !serviceReady || payloadLength > maxPayloadLength || + this->sendSequence == UINT64_MAX) + { + LG_UNLOCK(this->lock); + free(send); + return false; + } + + envelope.sessionEpoch = this->sessionEpoch; + envelope.sequence = ++this->sendSequence; + if (!lgNetPacketEncode(send->data, packetSize, &envelope, payload)) + { + LG_UNLOCK(this->lock); + free(send); + return false; + } + + send->buffer.Buffer = send->data; + send->buffer.Length = (uint32_t)packetSize; + + const QUIC_STATUS status = this->api->StreamSend(stream, + &send->buffer, 1, QUIC_SEND_FLAG_NONE, send); + LG_UNLOCK(this->lock); + + if (QUIC_FAILED(status)) + { + free(send); + return false; + } + return true; +} + +static bool networkConnectionSendHello(NetworkConnection * this) +{ + LGNetHello hello; + lgNetHelloInit(&hello, LG_NET_ROLE_CLIENT); + hello.maxPayloadLength = NETWORK_MAX_CONTROL_PAYLOAD; + hello.maxDatagramPayload = NETWORK_MAX_DATAGRAM_PAYLOAD; + if (this->password && *this->password) + { + DEBUG_ERROR("Network password authentication is not implemented yet"); + return false; + } + + uint8_t payload[LG_NET_HELLO_WIRE_SIZE]; + if (!lgNetHelloEncode(payload, sizeof(payload), &hello)) + return false; + + return networkConnectionSendPacket(this, LG_NET_SERVICE_CORE, + LG_NET_CORE_MESSAGE_HELLO, LG_NET_CORE_VERSION_CURRENT, + LG_NET_ENVELOPE_FINAL, 0, NETWORK_HELLO_REQUEST_ID, + payload, sizeof(payload), true); +} + +static void networkConnectionReady(NetworkConnection * this) +{ + LG_LOCK(this->stateCallbackLock); + LG_LOCK(this->lock); + if (!this->connecting || this->connectStatus == LG_TRANSPORT_OK || + atomic_load_explicit(&this->stopping, memory_order_acquire)) + { + LG_UNLOCK(this->lock); + LG_UNLOCK(this->stateCallbackLock); + return; + } + + this->connectStatus = LG_TRANSPORT_OK; + atomic_store_explicit(&this->sessionValid, true, memory_order_release); + LG_UNLOCK(this->lock); + + if (this->state) + this->state(this->opaque, true, LG_TRANSPORT_OK); + LG_UNLOCK(this->stateCallbackLock); + lgSignalEvent(this->connectEvent); +} + +static bool networkConnectionHandleCapability(NetworkConnection * this, + const LGNetPacketView * packet) +{ + LGNetCapability capability; + if (lgNetCapabilityDecode(&capability, + packet->payload, packet->payloadLength) != LG_NET_PARSE_OK) + return false; + + LG_LOCK(this->lock); + if (!this->helloAcknowledged || this->capabilitiesComplete || + this->receivedCapabilities >= this->expectedCapabilities || + capability.service <= LG_NET_SERVICE_CORE || + capability.service > LG_NET_SERVICE_USB || + !(capability.flags & LG_NET_CAPABILITY_SELECTED) || + capability.versionMin != networkServiceVersion(capability.service) || + capability.maxPayloadLength > this->maxPayloadLength || + capability.maxDatagramPayload > this->maxDatagramPayload || + (this->receivedServices & (UINT16_C(1) << capability.service))) + { + LG_UNLOCK(this->lock); + return false; + } + + this->receivedServices |= UINT16_C(1) << capability.service; + if (capability.flags & LG_NET_CAPABILITY_RELIABLE) + this->reliableServices |= UINT16_C(1) << capability.service; + this->serviceMaxPayload[capability.service] = + capability.maxPayloadLength; + this->serviceMaxDatagram[capability.service] = + capability.maxDatagramPayload; + ++this->receivedCapabilities; + LG_UNLOCK(this->lock); + return true; +} + +static bool networkConnectionHandleCore(NetworkConnection * this, + const LGNetPacketView * packet) +{ + const LGNetEnvelopeFlags responseFlags = + LG_NET_ENVELOPE_RESPONSE | + LG_NET_ENVELOPE_FINAL | + LG_NET_ENVELOPE_RELIABLE; + uint64_t sessionEpoch; + LG_LOCK(this->lock); + sessionEpoch = this->sessionEpoch; + LG_UNLOCK(this->lock); + + switch (packet->envelope.messageType) + { + case LG_NET_CORE_MESSAGE_HELLO_ACK: + { + LGNetHelloAck acknowledgement; + if (packet->envelope.flags != responseFlags || + packet->envelope.requestID != NETWORK_HELLO_REQUEST_ID || + packet->envelope.componentEpoch != 0 || + lgNetHelloAckDecode(&acknowledgement, + packet->payload, packet->payloadLength) != LG_NET_PARSE_OK || + acknowledgement.protocolVersion != + LG_NET_PROTOCOL_VERSION_CURRENT || + acknowledgement.authMode != LG_NET_AUTH_NONE || + acknowledgement.maxPayloadLength > NETWORK_MAX_CONTROL_PAYLOAD || + acknowledgement.maxDatagramPayload > + NETWORK_MAX_DATAGRAM_PAYLOAD || + packet->envelope.sessionEpoch != acknowledgement.sessionEpoch) + return false; + + LG_LOCK(this->lock); + if (this->helloAcknowledged) + { + LG_UNLOCK(this->lock); + return false; + } + + this->helloAcknowledged = true; + this->authenticationComplete = true; + this->expectedCapabilities = acknowledgement.capabilityCount; + this->sessionEpoch = acknowledgement.sessionEpoch; + this->maxPayloadLength = acknowledgement.maxPayloadLength; + this->maxDatagramPayload = acknowledgement.maxDatagramPayload; + LG_UNLOCK(this->lock); + return true; + } + + case LG_NET_CORE_MESSAGE_CAPABILITY: + if (packet->envelope.flags != responseFlags || + packet->envelope.requestID != NETWORK_HELLO_REQUEST_ID || + packet->envelope.componentEpoch != 0 || + packet->envelope.sessionEpoch != sessionEpoch) + return false; + return networkConnectionHandleCapability(this, packet); + + case LG_NET_CORE_MESSAGE_CAPABILITIES_DONE: + { + if (packet->envelope.flags != responseFlags || + packet->envelope.requestID != NETWORK_HELLO_REQUEST_ID || + packet->envelope.componentEpoch != 0 || + packet->envelope.sessionEpoch != sessionEpoch || + packet->payloadLength) + return false; + + bool ready; + LG_LOCK(this->lock); + ready = !this->capabilitiesComplete && + this->helloAcknowledged && + this->authenticationComplete && + this->receivedCapabilities == this->expectedCapabilities; + this->capabilitiesComplete = ready; + LG_UNLOCK(this->lock); + if (ready) + networkConnectionReady(this); + return ready; + } + + case LG_NET_CORE_MESSAGE_AUTH_CHALLENGE: + DEBUG_ERROR("The server requested unsupported password authentication"); + return false; + + case LG_NET_CORE_MESSAGE_AUTH_RESULT: + return false; + + default: + return false; + } +} + +static bool networkConnectionHandlePacket(NetworkConnection * this, + const LGNetPacketView * packet, bool datagram) +{ + if (packet->envelope.service == LG_NET_SERVICE_CORE) + return networkConnectionHandleCore(this, packet); + + if (!atomic_load_explicit(&this->sessionValid, memory_order_acquire)) + return false; + + LG_LOCK(this->lock); + const bool sessionValid = + packet->envelope.sessionEpoch == this->sessionEpoch && + (this->receivedServices & + (UINT16_C(1) << packet->envelope.service)) && + (this->reliableServices & + (UINT16_C(1) << packet->envelope.service)) && + packet->payloadLength <= + this->serviceMaxPayload[packet->envelope.service]; + LG_UNLOCK(this->lock); + if (!sessionValid) + return false; + + if (this->packet) + this->packet(this->opaque, packet, datagram); + return true; +} + +static bool networkConnectionValidateControl(NetworkConnection * this, + const LGNetPacketView * packet) +{ + uint32_t maxPayloadLength; + LG_LOCK(this->lock); + maxPayloadLength = this->maxPayloadLength; + LG_UNLOCK(this->lock); + + return packet->envelope.headerSize <= NETWORK_MAX_HEADER_SIZE && + packet->payloadLength <= maxPayloadLength && + packet->envelope.messageVersion == LG_NET_MESSAGE_VERSION_CURRENT && + (packet->envelope.flags & + (LG_NET_ENVELOPE_RELIABLE | LG_NET_ENVELOPE_FINAL)) == + (LG_NET_ENVELOPE_RELIABLE | LG_NET_ENVELOPE_FINAL) && + !(packet->envelope.flags & + (LG_NET_ENVELOPE_DATAGRAM | LG_NET_ENVELOPE_EARLY_DATA)) && + lgNetServiceKnown(packet->envelope.service) && + lgNetMessageKnown(packet->envelope.service, + packet->envelope.messageType) && + packet->envelope.serviceVersion == + networkServiceVersion(packet->envelope.service); +} + +static bool networkConnectionDecodeDatagram(NetworkConnection * this, + const QUIC_BUFFER * buffer, LGNetPacketView * packet) +{ + if (!buffer || !buffer->Buffer || !packet || + lgNetPacketDecode(packet, buffer->Buffer, + buffer->Length) != LG_NET_PARSE_OK || + packet->wireSize != buffer->Length || + packet->envelope.service == LG_NET_SERVICE_CORE || + !lgNetServiceKnown(packet->envelope.service) || + !lgNetMessageKnown(packet->envelope.service, + packet->envelope.messageType) || + packet->envelope.serviceVersion != + networkServiceVersion(packet->envelope.service)) + return false; + + uint64_t sessionEpoch; + uint16_t receivedServices; + uint32_t serviceMaxDatagram; + LG_LOCK(this->lock); + sessionEpoch = this->sessionEpoch; + receivedServices = this->receivedServices; + serviceMaxDatagram = + this->serviceMaxDatagram[packet->envelope.service]; + LG_UNLOCK(this->lock); + + return atomic_load_explicit( + &this->sessionValid, memory_order_acquire) && + (receivedServices & (UINT16_C(1) << packet->envelope.service)) && + serviceMaxDatagram && + packet->envelope.headerSize <= NETWORK_MAX_HEADER_SIZE && + packet->payloadLength <= serviceMaxDatagram && + packet->envelope.sessionEpoch == sessionEpoch && + packet->envelope.sequence != 0 && + packet->envelope.messageVersion == LG_NET_MESSAGE_VERSION_CURRENT && + (packet->envelope.flags & LG_NET_ENVELOPE_DATAGRAM) && + !(packet->envelope.flags & + (LG_NET_ENVELOPE_RELIABLE | LG_NET_ENVELOPE_EARLY_DATA)); +} + +static bool networkConnectionReserveReceive(NetworkConnection * this, + size_t additional) +{ + if (this->receiveSize > NETWORK_MAX_CONTROL_BUFFER || + additional > NETWORK_MAX_CONTROL_BUFFER - this->receiveSize) + return false; + + const size_t required = this->receiveSize + additional; + if (required <= this->receiveCapacity) + return true; + + size_t capacity = this->receiveCapacity ? this->receiveCapacity : 4096U; + while (capacity < required) + { + const size_t next = capacity * 2U; + if (next < capacity || next > NETWORK_MAX_CONTROL_BUFFER) + { + capacity = NETWORK_MAX_CONTROL_BUFFER; + break; + } + capacity = next; + } + + uint8_t * data = realloc(this->receiveData, capacity); + if (!data) + return false; + this->receiveData = data; + this->receiveCapacity = capacity; + return true; +} + +static bool networkConnectionReceive(NetworkConnection * this, + const QUIC_BUFFER * buffers, uint32_t count) +{ + if (count && !buffers) + return false; + + size_t additional = 0; + for (uint32_t i = 0; i < count; ++i) + { + if (buffers[i].Length && !buffers[i].Buffer) + return false; + if (buffers[i].Length > SIZE_MAX - additional) + return false; + additional += buffers[i].Length; + } + + if (!networkConnectionReserveReceive(this, additional)) + return false; + + for (uint32_t i = 0; i < count; ++i) + { + if (!buffers[i].Length) + continue; + memcpy(this->receiveData + this->receiveSize, + buffers[i].Buffer, buffers[i].Length); + this->receiveSize += buffers[i].Length; + } + + while (this->receiveSize >= LG_NET_ENVELOPE_WIRE_SIZE) + { + const uint16_t headerSize = networkReadU16(this->receiveData + 8U); + const uint32_t payloadSize = networkReadU32(this->receiveData + 24U); + 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 = (size_t)headerSize + payloadSize; + if (packetSize > this->receiveSize) + break; + + LGNetPacketView packet; + if (lgNetPacketDecode(&packet, this->receiveData, + packetSize) != LG_NET_PARSE_OK) + return false; + + LG_LOCK(this->lock); + const bool sequenceValid = this->receiveSequence != UINT64_MAX && + packet.envelope.sequence == this->receiveSequence + 1U; + if (sequenceValid) + this->receiveSequence = packet.envelope.sequence; + LG_UNLOCK(this->lock); + if (!sequenceValid || !networkConnectionValidateControl(this, &packet) || + !networkConnectionHandlePacket(this, &packet, false)) + return false; + + this->receiveSize -= packetSize; + memmove(this->receiveData, this->receiveData + packetSize, + this->receiveSize); + } + return true; +} + +static void networkConnectionTrustDecision(void * opaque, bool trusted) +{ + NetworkConnection * this = opaque; + + LG_LOCK(this->lock); + if (!this->certificatePending || !this->connection) + { + LG_UNLOCK(this->lock); + return; + } + trusted = trusted && !atomic_load_explicit( + &this->stopping, memory_order_acquire); + this->certificatePending = false; + HQUIC connection = this->connection; + char fingerprint[NETWORK_FINGERPRINT_TEXT_SIZE]; + memcpy(fingerprint, this->pendingFingerprint, sizeof(fingerprint)); + memset(this->pendingFingerprint, 0, sizeof(this->pendingFingerprint)); + LG_UNLOCK(this->lock); + + if (trusted) + trusted = networkKnownHostsRemember( + this->knownHosts, this->endpoint, fingerprint); + const QUIC_STATUS status = + this->api->ConnectionCertificateValidationComplete(connection, + trusted, trusted ? QUIC_TLS_ALERT_CODE_SUCCESS : + QUIC_TLS_ALERT_CODE_BAD_CERTIFICATE); + if (QUIC_FAILED(status)) + { + networkConnectionFail(this, LG_TRANSPORT_ERROR); + this->api->ConnectionShutdown(connection, + QUIC_CONNECTION_SHUTDOWN_FLAG_SILENT, 0); + } +} + +static QUIC_STATUS networkConnectionCertificate(NetworkConnection * this, + const QUIC_CONNECTION_EVENT * event) +{ + const QUIC_BUFFER * certificate = + (const QUIC_BUFFER *)event->PEER_CERTIFICATE_RECEIVED.Certificate; + if (!certificate || !certificate->Buffer || !certificate->Length) + return QUIC_STATUS_BAD_CERTIFICATE; + + uint8_t fingerprint[NETWORK_FINGERPRINT_SIZE]; + char fingerprintText[NETWORK_FINGERPRINT_TEXT_SIZE]; + if (!networkFingerprintSHA256(certificate->Buffer, + certificate->Length, fingerprint)) + return QUIC_STATUS_BAD_CERTIFICATE; + networkFingerprintFormat(fingerprint, fingerprintText); + + switch (networkKnownHostsCheck( + this->knownHosts, this->endpoint, fingerprintText)) + { + case NETWORK_KNOWN_HOST_MATCH: + return QUIC_STATUS_SUCCESS; + + case NETWORK_KNOWN_HOST_CHANGED: + DEBUG_ERROR("The certificate for %s changed; remove its entry from " + "network_known_hosts before trusting a replacement", + this->endpoint); + return QUIC_STATUS_BAD_CERTIFICATE; + + case NETWORK_KNOWN_HOST_ERROR: + DEBUG_ERROR("Failed to check the network server fingerprint"); + return QUIC_STATUS_BAD_CERTIFICATE; + + case NETWORK_KNOWN_HOST_UNKNOWN: + break; + } + + if (!this->trustPrompt) + return QUIC_STATUS_BAD_CERTIFICATE; + + LG_LOCK(this->lock); + if (this->certificatePending || + atomic_load_explicit(&this->stopping, memory_order_acquire)) + { + LG_UNLOCK(this->lock); + return QUIC_STATUS_BAD_CERTIFICATE; + } + this->certificatePending = true; + memcpy(this->pendingFingerprint, fingerprintText, + sizeof(this->pendingFingerprint)); + LG_UNLOCK(this->lock); + + if (!this->trustPrompt(this->opaque, this->endpoint, fingerprintText, + networkConnectionTrustDecision, this)) + { + LG_LOCK(this->lock); + const bool validationCompleted = !this->certificatePending; + if (!validationCompleted) + { + this->certificatePending = false; + memset(this->pendingFingerprint, 0, + sizeof(this->pendingFingerprint)); + } + LG_UNLOCK(this->lock); + return validationCompleted ? + QUIC_STATUS_PENDING : QUIC_STATUS_BAD_CERTIFICATE; + } + + /* MsQuic marks validation pending before invoking this callback and permits + * ConnectionCertificateValidationComplete from the callback. Always return + * PENDING once the prompt owns the decision, including during cancellation. */ + return QUIC_STATUS_PENDING; +} + +static QUIC_STATUS QUIC_API networkConnectionCallback(HQUIC handle, + void * opaque, QUIC_CONNECTION_EVENT * event) +{ + NetworkConnection * this = opaque; + + switch (event->Type) + { + case QUIC_CONNECTION_EVENT_CONNECTED: + { + if (atomic_load_explicit(&this->stopping, memory_order_acquire)) + { + this->api->ConnectionShutdown(handle, + QUIC_CONNECTION_SHUTDOWN_FLAG_SILENT, 0); + return QUIC_STATUS_SUCCESS; + } + + HQUIC stream = NULL; + QUIC_STATUS status = this->api->StreamOpen(handle, + QUIC_STREAM_OPEN_FLAG_NONE, networkStreamCallback, this, &stream); + if (QUIC_FAILED(status)) + { + networkConnectionFail(this, LG_TRANSPORT_ERROR); + this->api->ConnectionShutdown(handle, + QUIC_CONNECTION_SHUTDOWN_FLAG_SILENT, 0); + return QUIC_STATUS_SUCCESS; + } + + LG_LOCK(this->lock); + this->controlStream = stream; + LG_UNLOCK(this->lock); + status = this->api->StreamStart( + stream, QUIC_STREAM_START_FLAG_IMMEDIATE | + QUIC_STREAM_START_FLAG_SHUTDOWN_ON_FAIL); + if (QUIC_FAILED(status)) + { + networkConnectionFail(this, LG_TRANSPORT_ERROR); + this->api->ConnectionShutdown(handle, + QUIC_CONNECTION_SHUTDOWN_FLAG_SILENT, 0); + } + return QUIC_STATUS_SUCCESS; + } + + case QUIC_CONNECTION_EVENT_SHUTDOWN_INITIATED_BY_TRANSPORT: + networkConnectionFail(this, LG_TRANSPORT_UNAVAILABLE); + return QUIC_STATUS_SUCCESS; + + case QUIC_CONNECTION_EVENT_SHUTDOWN_INITIATED_BY_PEER: + networkConnectionFail(this, LG_TRANSPORT_DISCONNECTED); + return QUIC_STATUS_SUCCESS; + + case QUIC_CONNECTION_EVENT_SHUTDOWN_COMPLETE: + networkConnectionSetState( + this, false, LG_TRANSPORT_DISCONNECTED); + lgSignalEvent(this->connectEvent); + lgSignalEvent(this->closeEvent); + return QUIC_STATUS_SUCCESS; + + case QUIC_CONNECTION_EVENT_PEER_STREAM_STARTED: + this->api->SetCallbackHandler(event->PEER_STREAM_STARTED.Stream, + (void *)networkRejectedStreamCallback, this); + this->api->StreamShutdown(event->PEER_STREAM_STARTED.Stream, + QUIC_STREAM_SHUTDOWN_FLAG_ABORT | + QUIC_STREAM_SHUTDOWN_FLAG_IMMEDIATE, 0); + return QUIC_STATUS_SUCCESS; + + case QUIC_CONNECTION_EVENT_DATAGRAM_RECEIVED: + { + LGNetPacketView packet; + if (networkConnectionDecodeDatagram( + this, event->DATAGRAM_RECEIVED.Buffer, &packet) && this->packet) + this->packet(this->opaque, &packet, true); + return QUIC_STATUS_SUCCESS; + } + + case QUIC_CONNECTION_EVENT_PEER_CERTIFICATE_RECEIVED: + return networkConnectionCertificate(this, event); + + default: + return QUIC_STATUS_SUCCESS; + } +} + +static QUIC_STATUS QUIC_API networkStreamCallback(HQUIC handle, + void * opaque, QUIC_STREAM_EVENT * event) +{ + NetworkConnection * this = opaque; + + switch (event->Type) + { + case QUIC_STREAM_EVENT_START_COMPLETE: + if (QUIC_FAILED(event->START_COMPLETE.Status) || + !networkConnectionSendHello(this)) + { + networkConnectionFail(this, LG_TRANSPORT_ERROR); + this->api->ConnectionShutdown(this->connection, + QUIC_CONNECTION_SHUTDOWN_FLAG_SILENT, 0); + } + return QUIC_STATUS_SUCCESS; + + case QUIC_STREAM_EVENT_RECEIVE: + if (!networkConnectionReceive( + this, event->RECEIVE.Buffers, event->RECEIVE.BufferCount)) + { + networkConnectionFail(this, LG_TRANSPORT_ERROR); + this->api->ConnectionShutdown(this->connection, + QUIC_CONNECTION_SHUTDOWN_FLAG_SILENT, 0); + } + return QUIC_STATUS_SUCCESS; + + case QUIC_STREAM_EVENT_SEND_COMPLETE: + free(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: + this->api->ConnectionShutdown(this->connection, + QUIC_CONNECTION_SHUTDOWN_FLAG_SILENT, 0); + return QUIC_STATUS_SUCCESS; + + case QUIC_STREAM_EVENT_SHUTDOWN_COMPLETE: + LG_LOCK(this->lock); + if (this->controlStream == handle) + this->controlStream = NULL; + LG_UNLOCK(this->lock); + this->api->StreamClose(handle); + return QUIC_STATUS_SUCCESS; + + default: + return QUIC_STATUS_SUCCESS; + } +} + +static QUIC_STATUS QUIC_API networkRejectedStreamCallback(HQUIC handle, + void * opaque, QUIC_STREAM_EVENT * event) +{ + NetworkConnection * this = opaque; + if (event->Type == QUIC_STREAM_EVENT_SHUTDOWN_COMPLETE) + this->api->StreamClose(handle); + return QUIC_STATUS_SUCCESS; +} + +bool networkConnectionCreate(NetworkConnection ** result, + const NetworkConnectionParams * params) +{ + if (!result || !params || !params->address || !params->knownHosts || + (!!params->trustPrompt != !!params->trustCancel)) + return false; + + NetworkConnection * this = calloc(1, sizeof(*this)); + if (!this) + return false; + + LG_LOCK_INIT(this->lock); + LG_LOCK_INIT(this->stateCallbackLock); + atomic_init(&this->stopping, false); + atomic_init(&this->sessionValid, false); + + this->address = networkDuplicate(params->address); + this->password = networkDuplicate(params->password); + this->endpoint = networkEndpoint(params->address, params->port); + if (!this->address || (params->password && !this->password) || + !this->endpoint) + goto fail; + + this->port = params->port; + this->knownHosts = params->knownHosts; + this->trustPrompt = params->trustPrompt; + this->trustCancel = params->trustCancel; + this->packet = params->packet; + this->state = params->state; + this->opaque = params->opaque; + + this->connectEvent = lgCreateEvent(false, 0); + this->closeEvent = lgCreateEvent(false, 0); + if (!this->connectEvent || !this->closeEvent) + goto fail; + + if (QUIC_FAILED(MsQuicOpen2(&this->api))) + goto fail; + + const QUIC_REGISTRATION_CONFIG registration = + { + .AppName = "looking-glass-client", + .ExecutionProfile = QUIC_EXECUTION_PROFILE_LOW_LATENCY, + }; + if (QUIC_FAILED(this->api->RegistrationOpen( + ®istration, &this->registration))) + goto fail; + + const QUIC_BUFFER alpn = + { + .Length = LG_NET_ALPN_LENGTH, + .Buffer = (uint8_t *)LG_NET_ALPN, + }; + const QUIC_SETTINGS settings = + { + .IsSet = + { + .HandshakeIdleTimeoutMs = true, + .IdleTimeoutMs = true, + .InitialRttMs = true, + .KeepAliveIntervalMs = true, + .PeerBidiStreamCount = true, + .PeerUnidiStreamCount = true, + .DatagramReceiveEnabled = true, + }, + .HandshakeIdleTimeoutMs = NETWORK_HANDSHAKE_TIMEOUT_MS, + .IdleTimeoutMs = NETWORK_IDLE_TIMEOUT_MS, + .InitialRttMs = NETWORK_INITIAL_RTT_MS, + .KeepAliveIntervalMs = NETWORK_KEEPALIVE_INTERVAL_MS, + .PeerBidiStreamCount = NETWORK_MAX_PEER_STREAMS, + .PeerUnidiStreamCount = NETWORK_MAX_PEER_STREAMS, + .DatagramReceiveEnabled = true, + }; + if (QUIC_FAILED(this->api->ConfigurationOpen(this->registration, + &alpn, 1, &settings, sizeof(settings), NULL, + &this->configuration))) + goto fail; + + /* PKI chain validation is disabled because identity is enforced by the + * certificate callback and the persistent TOFU store above. */ + const QUIC_CREDENTIAL_CONFIG credential = + { + .Type = QUIC_CREDENTIAL_TYPE_NONE, + .Flags = QUIC_CREDENTIAL_FLAG_CLIENT | + QUIC_CREDENTIAL_FLAG_NO_CERTIFICATE_VALIDATION | + QUIC_CREDENTIAL_FLAG_INDICATE_CERTIFICATE_RECEIVED | + QUIC_CREDENTIAL_FLAG_USE_PORTABLE_CERTIFICATES, + }; + if (QUIC_FAILED(this->api->ConfigurationLoadCredential( + this->configuration, &credential))) + goto fail; + + *result = this; + return true; + +fail: + networkConnectionDestroy(&this); + return false; +} + +void networkConnectionDestroy(NetworkConnection ** connection) +{ + if (!connection || !*connection) + return; + + NetworkConnection * this = *connection; + networkConnectionDisconnect(this); + if (this->configuration) + this->api->ConfigurationClose(this->configuration); + if (this->registration) + this->api->RegistrationClose(this->registration); + if (this->api) + MsQuicClose(this->api); + if (this->closeEvent) + lgFreeEvent(this->closeEvent); + if (this->connectEvent) + lgFreeEvent(this->connectEvent); + LG_LOCK_FREE(this->stateCallbackLock); + LG_LOCK_FREE(this->lock); + free(this->receiveData); + free(this->endpoint); + free(this->password); + free(this->address); + free(this); + *connection = NULL; +} + +LG_TransportStatus networkConnectionConnect(NetworkConnection * this, + LG_TransportSession * session, LG_TransportCancelledFn cancelled, + void * cancelledOpaque) +{ + if (!this || !session || this->connection) + return LG_TRANSPORT_ERROR; + if (this->password && *this->password) + { + DEBUG_ERROR("network:password requires a future password proof provider"); + return LG_TRANSPORT_ERROR; + } + if (cancelled && cancelled(cancelledOpaque)) + return LG_TRANSPORT_DISCONNECTED; + + atomic_store_explicit(&this->stopping, false, memory_order_release); + atomic_store_explicit(&this->sessionValid, false, memory_order_release); + lgResetEvent(this->connectEvent); + lgResetEvent(this->closeEvent); + + LG_LOCK(this->lock); + this->connectStatus = LG_TRANSPORT_UNAVAILABLE; + this->connecting = true; + this->sessionEpoch = 0; + this->sendSequence = 0; + this->receiveSequence = 0; + this->helloAcknowledged = false; + this->authenticationComplete = false; + this->capabilitiesComplete = false; + this->connectFailed = false; + this->expectedCapabilities = 0; + this->receivedCapabilities = 0; + this->receivedServices = 0; + this->reliableServices = 0; + this->maxPayloadLength = NETWORK_MAX_CONTROL_PAYLOAD; + this->maxDatagramPayload = 0; + memset(this->serviceMaxPayload, 0, sizeof(this->serviceMaxPayload)); + memset(this->serviceMaxDatagram, 0, sizeof(this->serviceMaxDatagram)); + this->receiveSize = 0; + memset(&this->session, 0, sizeof(this->session)); + snprintf(this->session.version, sizeof(this->session.version), + "LG network protocol %u", LG_NET_PROTOCOL_VERSION_CURRENT); + snprintf(this->session.name, sizeof(this->session.name), + "%s", this->endpoint); + this->session.os = LG_TRANSPORT_OS_OTHER; + LG_UNLOCK(this->lock); + + QUIC_STATUS status = this->api->ConnectionOpen(this->registration, + networkConnectionCallback, this, &this->connection); + if (QUIC_FAILED(status)) + { + LG_LOCK(this->lock); + this->connecting = false; + this->connectStatus = LG_TRANSPORT_ERROR; + LG_UNLOCK(this->lock); + this->connection = NULL; + return LG_TRANSPORT_ERROR; + } + + status = this->api->ConnectionStart(this->connection, this->configuration, + QUIC_ADDRESS_FAMILY_UNSPEC, this->address, this->port); + if (QUIC_FAILED(status)) + { + this->api->ConnectionClose(this->connection); + this->connection = NULL; + LG_LOCK(this->lock); + this->connecting = false; + this->connectStatus = LG_TRANSPORT_UNAVAILABLE; + LG_UNLOCK(this->lock); + return LG_TRANSPORT_UNAVAILABLE; + } + + while (!lgWaitEvent(this->connectEvent, NETWORK_CONNECT_POLL_MS)) + if (cancelled && cancelled(cancelledOpaque)) + { + networkConnectionCancel(this); + break; + } + + LG_LOCK(this->lock); + const LG_TransportStatus connectStatus = this->connectStatus; + this->connecting = false; + if (connectStatus == LG_TRANSPORT_OK) + *session = this->session; + LG_UNLOCK(this->lock); + + if (connectStatus != LG_TRANSPORT_OK) + networkConnectionDisconnect(this); + return connectStatus; +} + +void networkConnectionCancel(NetworkConnection * this) +{ + if (!this) + return; + + atomic_store_explicit(&this->stopping, true, memory_order_release); + if (this->trustCancel) + this->trustCancel(this->opaque); + + LG_LOCK(this->stateCallbackLock); + LG_LOCK(this->lock); + HQUIC connection = this->connection; + const bool certificatePending = + this->certificatePending && connection; + atomic_store_explicit(&this->sessionValid, false, memory_order_release); + this->certificatePending = false; + memset(this->pendingFingerprint, 0, sizeof(this->pendingFingerprint)); + if (this->connecting) + { + this->connectStatus = LG_TRANSPORT_DISCONNECTED; + this->connectFailed = true; + } + LG_UNLOCK(this->lock); + LG_UNLOCK(this->stateCallbackLock); + if (certificatePending) + this->api->ConnectionCertificateValidationComplete(connection, false, + QUIC_TLS_ALERT_CODE_BAD_CERTIFICATE); + if (connection) + this->api->ConnectionShutdown(connection, + QUIC_CONNECTION_SHUTDOWN_FLAG_SILENT, 0); + if (this->connectEvent) + lgSignalEvent(this->connectEvent); +} + +void networkConnectionDisconnect(NetworkConnection * this) +{ + if (!this) + return; + + networkConnectionCancel(this); + + LG_LOCK(this->lock); + HQUIC connection = this->connection; + LG_UNLOCK(this->lock); + if (!connection) + return; + + lgWaitEvent(this->closeEvent, TIMEOUT_INFINITE); + LG_LOCK(this->stateCallbackLock); + LG_LOCK(this->lock); + const bool ownsConnection = this->connection == connection; + if (ownsConnection) + { + this->connection = NULL; + this->controlStream = NULL; + this->certificatePending = false; + memset(this->pendingFingerprint, 0, sizeof(this->pendingFingerprint)); + } + LG_UNLOCK(this->lock); + + if (ownsConnection) + { + this->api->ConnectionClose(connection); + atomic_store_explicit( + &this->sessionValid, false, memory_order_release); + if (this->state) + this->state(this->opaque, false, LG_TRANSPORT_DISCONNECTED); + } + LG_UNLOCK(this->stateCallbackLock); +} + +bool networkConnectionValid(const NetworkConnection * this) +{ + return this && atomic_load_explicit( + &this->sessionValid, memory_order_acquire); +} + +LG_TransportStatus networkConnectionSend(NetworkConnection * this, + LGNetService service, uint16_t messageType, uint16_t serviceVersion, + LGNetEnvelopeFlags flags, uint64_t componentEpoch, uint64_t requestID, + const void * payload, uint32_t payloadLength) +{ + if (!this || !networkConnectionValid(this)) + return LG_TRANSPORT_DISCONNECTED; + + return networkConnectionSendPacket(this, service, messageType, + serviceVersion, flags, componentEpoch, requestID, + payload, payloadLength, false) ? + LG_TRANSPORT_OK : LG_TRANSPORT_ERROR; +} diff --git a/client/transports/Network/connection.h b/client/transports/Network/connection.h new file mode 100644 index 00000000..b1107247 --- /dev/null +++ b/client/transports/Network/connection.h @@ -0,0 +1,73 @@ +/** + * 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 "known_hosts.h" + +#include "interface/transport.h" + +#include "common/network/protocol.h" + +#include +#include + +typedef struct NetworkConnection NetworkConnection; + +typedef void (*NetworkTrustDecisionFn)(void * opaque, bool trusted); +typedef bool (*NetworkTrustPromptFn)(void * opaque, const char * endpoint, + const char * fingerprint, NetworkTrustDecisionFn decision, + void * decisionOpaque); +typedef void (*NetworkTrustCancelFn)(void * opaque); +/* The packet view and payload are borrowed for the duration of the callback. */ +typedef void (*NetworkPacketFn)(void * opaque, + const LGNetPacketView * packet, bool datagram); +typedef void (*NetworkConnectionStateFn)(void * opaque, bool connected, + LG_TransportStatus reason); + +typedef struct NetworkConnectionParams +{ + const char * address; + uint16_t port; + const char * password; + NetworkKnownHosts * knownHosts; + NetworkTrustPromptFn trustPrompt; + NetworkTrustCancelFn trustCancel; + NetworkPacketFn packet; + NetworkConnectionStateFn state; + void * opaque; +} +NetworkConnectionParams; + +bool networkConnectionCreate(NetworkConnection ** connection, + const NetworkConnectionParams * params); +void networkConnectionDestroy(NetworkConnection ** connection); + +LG_TransportStatus networkConnectionConnect(NetworkConnection * connection, + LG_TransportSession * session, LG_TransportCancelledFn cancelled, + void * cancelledOpaque); +void networkConnectionCancel(NetworkConnection * connection); +void networkConnectionDisconnect(NetworkConnection * connection); +bool networkConnectionValid(const NetworkConnection * connection); + +LG_TransportStatus networkConnectionSend(NetworkConnection * connection, + LGNetService service, uint16_t messageType, uint16_t serviceVersion, + LGNetEnvelopeFlags flags, uint64_t componentEpoch, uint64_t requestID, + const void * payload, uint32_t payloadLength); diff --git a/client/transports/Network/fingerprint.h b/client/transports/Network/fingerprint.h new file mode 100644 index 00000000..0ffa0e5d --- /dev/null +++ b/client/transports/Network/fingerprint.h @@ -0,0 +1,30 @@ +/** + * 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 +#include +#include + +#define NETWORK_FINGERPRINT_SIZE 32U + +bool networkFingerprintSHA256(const uint8_t * data, size_t size, + uint8_t fingerprint[NETWORK_FINGERPRINT_SIZE]); diff --git a/client/transports/Network/known_hosts.c b/client/transports/Network/known_hosts.c new file mode 100644 index 00000000..43f16cb2 --- /dev/null +++ b/client/transports/Network/known_hosts.c @@ -0,0 +1,254 @@ +/** + * 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 "known_hosts.h" +#include "platform.h" + +#include "common/debug.h" +#include "common/filesystem.h" +#include "common/paths.h" + +#include +#include +#include + +#define NETWORK_KNOWN_HOSTS_FILE "network_known_hosts" +#define NETWORK_KNOWN_HOST_LINE 1024U + +static char * knownHostsPath(const char * suffix) +{ + const char * directory = lgConfigDir(); + const size_t dirLength = strlen(directory); + const size_t nameLength = strlen(NETWORK_KNOWN_HOSTS_FILE); + const size_t suffixLength = strlen(suffix); + char * path = malloc( + dirLength + 1U + nameLength + suffixLength + 1U); + if (!path) + return NULL; + + snprintf(path, dirLength + 1U + nameLength + suffixLength + 1U, + "%s/%s%s", directory, NETWORK_KNOWN_HOSTS_FILE, suffix); + return path; +} + +static bool validField(const char * value) +{ + if (!value || !*value) + return false; + + for (const unsigned char * c = (const unsigned char *)value; *c; ++c) + if (*c == '\r' || *c == '\n' || *c == '\t') + return false; + return true; +} + +static bool completeLine(const char * line, FILE * file) +{ + const size_t length = strlen(line); + return (length && line[length - 1U] == '\n') || feof(file); +} + +static bool writeLine(FILE * file, const char * line) +{ + const size_t length = strlen(line); + if (!length) + return false; + return fputs(line, file) >= 0 && + (line[length - 1U] == '\n' || fputc('\n', file) != EOF); +} + +bool networkKnownHostsInit(NetworkKnownHosts * knownHosts) +{ + if (!knownHosts) + return false; + + memset(knownHosts, 0, sizeof(*knownHosts)); + LG_LOCK_INIT(knownHosts->lock); + knownHosts->path = knownHostsPath(""); + return knownHosts->path != NULL; +} + +void networkKnownHostsFree(NetworkKnownHosts * knownHosts) +{ + if (!knownHosts) + return; + + LG_LOCK_FREE(knownHosts->lock); + free(knownHosts->path); + knownHosts->path = NULL; +} + +NetworkKnownHostResult networkKnownHostsCheck(NetworkKnownHosts * knownHosts, + const char * endpoint, const char * fingerprint) +{ + if (!knownHosts || !knownHosts->path || !validField(endpoint) || + !validField(fingerprint)) + return NETWORK_KNOWN_HOST_ERROR; + + LG_LOCK(knownHosts->lock); + FILE * file = lgOpenFile(knownHosts->path, "r"); + if (!file) + { + const LGFileType type = lgPathType(knownHosts->path); + LG_UNLOCK(knownHosts->lock); + return type == LG_FILE_TYPE_MISSING ? + NETWORK_KNOWN_HOST_UNKNOWN : NETWORK_KNOWN_HOST_ERROR; + } + + NetworkKnownHostResult result = NETWORK_KNOWN_HOST_UNKNOWN; + char line[NETWORK_KNOWN_HOST_LINE]; + while (fgets(line, sizeof(line), file)) + { + if (!completeLine(line, file)) + { + result = NETWORK_KNOWN_HOST_ERROR; + break; + } + + char * separator = strchr(line, '\t'); + if (!separator) + continue; + + *separator++ = '\0'; + separator[strcspn(separator, "\r\n")] = '\0'; + if (strcmp(line, endpoint) != 0) + continue; + + if (strcmp(separator, fingerprint) != 0) + { + result = NETWORK_KNOWN_HOST_CHANGED; + break; + } + result = NETWORK_KNOWN_HOST_MATCH; + } + + if (ferror(file)) + result = NETWORK_KNOWN_HOST_ERROR; + + fclose(file); + LG_UNLOCK(knownHosts->lock); + return result; +} + +static bool rewriteKnownHosts(NetworkKnownHosts * knownHosts, + const char * endpoint, const char * fingerprint) +{ + if (!knownHosts || !knownHosts->path || !validField(endpoint) || + (fingerprint && !validField(fingerprint))) + return false; + + char * temporary = knownHostsPath(".tmp"); + if (!temporary) + return false; + + LG_LOCK(knownHosts->lock); + FILE * source = lgOpenFile(knownHosts->path, "r"); + FILE * target = lgOpenFile(temporary, "w"); + bool found = false; + bool result = target && + (source || lgPathType(knownHosts->path) == LG_FILE_TYPE_MISSING); + char line[NETWORK_KNOWN_HOST_LINE]; + while (result && source && fgets(line, sizeof(line), source)) + { + if (!completeLine(line, source)) + { + result = false; + break; + } + + char * separator = strchr(line, '\t'); + if (!separator) + { + result = writeLine(target, line); + continue; + } + + *separator++ = '\0'; + if (strcmp(line, endpoint) != 0) + { + *--separator = '\t'; + result = writeLine(target, line); + continue; + } + + if (!fingerprint) + continue; + + separator[strcspn(separator, "\r\n")] = '\0'; + if (strcmp(separator, fingerprint) != 0) + { + result = false; + break; + } + found = true; + result = fprintf(target, "%s\t%s\n", endpoint, fingerprint) > 0; + } + + if (source && ferror(source)) + result = false; + if (result && fingerprint && !found) + result = fprintf(target, "%s\t%s\n", endpoint, fingerprint) > 0; + + if (target) + { + result = result && fflush(target) == 0; + result = fclose(target) == 0 && result; + } + if (source) + fclose(source); + + if (result) + result = networkPlatformReplaceFile(temporary, knownHosts->path); + + if (!result) + lgDeleteFile(temporary); + LG_UNLOCK(knownHosts->lock); + free(temporary); + return result; +} + +bool networkKnownHostsRemember(NetworkKnownHosts * knownHosts, + const char * endpoint, const char * fingerprint) +{ + const bool result = rewriteKnownHosts( + knownHosts, endpoint, fingerprint); + if (!result) + DEBUG_ERROR("Failed to persist the network server fingerprint"); + return result; +} + +bool networkKnownHostsForget(NetworkKnownHosts * knownHosts, + const char * endpoint) +{ + return rewriteKnownHosts(knownHosts, endpoint, NULL); +} + +void networkFingerprintFormat( + const uint8_t fingerprint[NETWORK_FINGERPRINT_SIZE], + char text[NETWORK_FINGERPRINT_TEXT_SIZE]) +{ + static const char digits[] = "0123456789abcdef"; + for (size_t i = 0; i < NETWORK_FINGERPRINT_SIZE; ++i) + { + text[i * 2U ] = digits[fingerprint[i] >> 4]; + text[i * 2U + 1] = digits[fingerprint[i] & 0xf]; + } + text[NETWORK_FINGERPRINT_TEXT_SIZE - 1U] = '\0'; +} diff --git a/client/transports/Network/known_hosts.h b/client/transports/Network/known_hosts.h new file mode 100644 index 00000000..613c468a --- /dev/null +++ b/client/transports/Network/known_hosts.h @@ -0,0 +1,59 @@ +/** + * 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 "fingerprint.h" + +#include "common/locking.h" + +#include +#include + +#define NETWORK_FINGERPRINT_TEXT_SIZE (NETWORK_FINGERPRINT_SIZE * 2U + 1U) + +typedef enum NetworkKnownHostResult +{ + NETWORK_KNOWN_HOST_UNKNOWN, + NETWORK_KNOWN_HOST_MATCH, + NETWORK_KNOWN_HOST_CHANGED, + NETWORK_KNOWN_HOST_ERROR, +} +NetworkKnownHostResult; + +typedef struct NetworkKnownHosts +{ + LG_Lock lock; + char * path; +} +NetworkKnownHosts; + +bool networkKnownHostsInit(NetworkKnownHosts * knownHosts); +void networkKnownHostsFree(NetworkKnownHosts * knownHosts); +NetworkKnownHostResult networkKnownHostsCheck(NetworkKnownHosts * knownHosts, + const char * endpoint, const char * fingerprint); +bool networkKnownHostsRemember(NetworkKnownHosts * knownHosts, + const char * endpoint, const char * fingerprint); +bool networkKnownHostsForget(NetworkKnownHosts * knownHosts, + const char * endpoint); + +void networkFingerprintFormat( + const uint8_t fingerprint[NETWORK_FINGERPRINT_SIZE], + char text[NETWORK_FINGERPRINT_TEXT_SIZE]); diff --git a/client/transports/Network/network.c b/client/transports/Network/network.c new file mode 100644 index 00000000..13cfc867 --- /dev/null +++ b/client/transports/Network/network.c @@ -0,0 +1,569 @@ +/** + * 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 "network.h" +#include "connection.h" +#include "known_hosts.h" + +#include "app.h" +#include "interface/renderer.h" + +#include "common/debug.h" +#include "common/event.h" +#include "common/locking.h" +#include "common/network/protocol.h" +#include "common/option.h" + +#include +#include +#include +#include +#include + +#define NETWORK_ADDRESS_MAX 255U +#define NETWORK_ENDPOINT_TEXT_SIZE (NETWORK_ADDRESS_MAX + 9U) + +struct NetworkTrustPrompt +{ + MsgBoxHandle message; + NetworkTrustDecisionFn decision; + void * decisionOpaque; +}; + +struct LG_Transport +{ + NetworkConnection * connection; + NetworkKnownHosts knownHosts; + LGEvent * frameEvent; + LGEvent * promptIdle; + + LG_Lock callbackLock; + LG_Lock stateLock; + LG_Lock promptLock; + + LG_VideoStatus videoStatus; + LG_VideoStatusFn videoStatusFn; + void * videoStatusOpaque; + + bool rendererAttached; + bool promptCallbackActive; + struct NetworkTrustPrompt prompt; +}; + +static bool networkAddressValidate(struct Option * option, + const char ** error) +{ + const char * address = option->value.x_string; + if (address && *address && strlen(address) <= NETWORK_ADDRESS_MAX) + { + for (const unsigned char * c = (const unsigned char *)address; *c; ++c) + if (*c < 0x20U || *c == 0x7fU) + goto invalid; + return true; + } + +invalid: + *error = "The network address must be 1-255 characters without controls"; + return false; +} + +static bool networkPortValidate(struct Option * option, const char ** error) +{ + if (option->value.x_int > 0 && option->value.x_int <= UINT16_MAX) + return true; + + *error = "The network port must be between 1 and 65535"; + return false; +} + +static void networkSetup(void) +{ + static struct Option options[] = + { + { + .module = "network", + .name = "address", + .description = "Network server address", + .type = OPTION_TYPE_STRING, + .value.x_string = "127.0.0.1", + .validator = networkAddressValidate, + }, + { + .module = "network", + .name = "port", + .description = "Network server UDP port", + .type = OPTION_TYPE_INT, + .value.x_int = LG_NET_DEFAULT_PORT, + .validator = networkPortValidate, + }, + { + .module = "network", + .name = "password", + .description = + "Reserved network password (empty uses no authentication)", + .type = OPTION_TYPE_STRING, + .value.x_string = NULL, + }, + { + .module = "network", + .name = "forgetCertificate", + .description = "Forget the saved certificate for this server", + .type = OPTION_TYPE_BOOL, + .value.x_bool = false, + }, + {0} + }; + + option_register(options); +} + +static void networkTrustDecision(bool trusted, void * opaque) +{ + LG_Transport * this = opaque; + NetworkTrustDecisionFn decision; + void * decisionOpaque; + + LG_LOCK(this->promptLock); + decision = this->prompt.decision; + decisionOpaque = this->prompt.decisionOpaque; + this->prompt.message = NULL; + this->prompt.decision = NULL; + this->prompt.decisionOpaque = NULL; + if (decision) + { + this->promptCallbackActive = true; + lgResetEvent(this->promptIdle); + } + LG_UNLOCK(this->promptLock); + + if (decision) + decision(decisionOpaque, trusted); + + LG_LOCK(this->promptLock); + this->promptCallbackActive = false; + lgSignalEvent(this->promptIdle); + LG_UNLOCK(this->promptLock); +} + +static bool networkTrustPrompt(void * opaque, const char * endpoint, + const char * fingerprint, NetworkTrustDecisionFn decision, + void * decisionOpaque) +{ + LG_Transport * this = opaque; + + LG_LOCK(this->promptLock); + if (this->prompt.message || this->prompt.decision || + this->promptCallbackActive) + { + LG_UNLOCK(this->promptLock); + return false; + } + + this->prompt.decision = decision; + this->prompt.decisionOpaque = decisionOpaque; + this->prompt.message = app_confirmMsgBox( + "Trust network server?", networkTrustDecision, this, + "The identity of %s has not been seen before.\n\n" + "SHA-256 fingerprint:\n%s\n\n" + "Trust and remember this server?", endpoint, fingerprint); + const bool queued = this->prompt.message != NULL; + if (!queued) + { + this->prompt.decision = NULL; + this->prompt.decisionOpaque = NULL; + } + LG_UNLOCK(this->promptLock); + return queued; +} + +static void networkCancelTrustPrompt(void * opaque) +{ + LG_Transport * this = opaque; + MsgBoxHandle message; + + LG_LOCK(this->promptLock); + message = this->prompt.message; + this->prompt.message = NULL; + this->prompt.decision = NULL; + this->prompt.decisionOpaque = NULL; + LG_UNLOCK(this->promptLock); + + if (message) + app_msgBoxClose(message); + lgWaitEvent(this->promptIdle, TIMEOUT_INFINITE); + LG_LOCK(this->promptLock); + LG_UNLOCK(this->promptLock); +} + +static void networkNotifyVideoStatus(LG_Transport * this) +{ + LG_VideoStatusFn callback; + void * opaque; + LG_VideoStatus status; + + LG_LOCK(this->callbackLock); + LG_LOCK(this->stateLock); + callback = this->videoStatusFn; + opaque = this->videoStatusOpaque; + status = this->videoStatus; + LG_UNLOCK(this->stateLock); + + if (callback) + callback(opaque, &status); + LG_UNLOCK(this->callbackLock); +} + +static uint64_t networkNextEpoch(uint64_t epoch) +{ + ++epoch; + return epoch ? epoch : 1U; +} + +static void networkSetUnavailable(LG_VideoComponentStatus * status, + LG_TransportStatus reason) +{ + if (status->available) + status->epoch = networkNextEpoch(status->epoch); + status->available = false; + status->reason = reason; +} + +static void networkConnectionState(void * opaque, bool connected, + LG_TransportStatus reason) +{ + LG_Transport * this = opaque; + const LG_TransportStatus status = connected ? + LG_TRANSPORT_UNAVAILABLE : reason; + + LG_LOCK(this->stateLock); + networkSetUnavailable(&this->videoStatus.frame, status); + networkSetUnavailable(&this->videoStatus.pointer, status); + LG_UNLOCK(this->stateLock); + + lgSignalEvent(this->frameEvent); + networkNotifyVideoStatus(this); +} + +static void networkPacket(void * opaque, const LGNetPacketView * packet, + bool datagram) +{ + (void)opaque; + (void)packet; + (void)datagram; + /* Media and service dispatch is added behind this boundary in the next + * implementation stage. The connection still validates all wire framing. */ +} + +static bool networkCreate(LG_Transport ** result) +{ + if (!result) + return false; + + LG_Transport * this = calloc(1, sizeof(*this)); + if (!this) + return false; + + LG_LOCK_INIT(this->callbackLock); + LG_LOCK_INIT(this->stateLock); + LG_LOCK_INIT(this->promptLock); + + this->videoStatus.frame.epoch = 1; + this->videoStatus.frame.reason = LG_TRANSPORT_DISCONNECTED; + this->videoStatus.pointer.epoch = 1; + this->videoStatus.pointer.reason = LG_TRANSPORT_DISCONNECTED; + if (!networkKnownHostsInit(&this->knownHosts)) + goto fail; + + this->frameEvent = lgCreateEvent(true, 0); + this->promptIdle = lgCreateEvent(false, 0); + if (!this->frameEvent || !this->promptIdle || + !lgSignalEvent(this->promptIdle)) + goto fail; + + const char * address = option_get_string("network", "address"); + const uint16_t port = (uint16_t)option_get_int("network", "port"); + char endpoint[NETWORK_ENDPOINT_TEXT_SIZE]; + if (!address) + goto fail; + snprintf(endpoint, sizeof(endpoint), "[%s]:%u", address, port); + if (option_get_bool("network", "forgetCertificate") && + !networkKnownHostsForget(&this->knownHosts, endpoint)) + { + DEBUG_ERROR("Failed to forget the network server certificate"); + goto fail; + } + + const NetworkConnectionParams params = + { + .address = address, + .port = port, + .password = option_get_string("network", "password"), + .knownHosts = &this->knownHosts, + .trustPrompt = networkTrustPrompt, + .trustCancel = networkCancelTrustPrompt, + .packet = networkPacket, + .state = networkConnectionState, + .opaque = this, + }; + if (!networkConnectionCreate(&this->connection, ¶ms)) + goto fail; + + *result = this; + return true; + +fail: + if (this->frameEvent) + lgFreeEvent(this->frameEvent); + if (this->promptIdle) + lgFreeEvent(this->promptIdle); + networkKnownHostsFree(&this->knownHosts); + LG_LOCK_FREE(this->promptLock); + LG_LOCK_FREE(this->stateLock); + LG_LOCK_FREE(this->callbackLock); + free(this); + return false; +} + +static void networkDisconnect(LG_Transport * this); + +static void networkDestroy(LG_Transport ** transport) +{ + if (!transport || !*transport) + return; + + LG_Transport * this = *transport; + networkDisconnect(this); + + LG_LOCK(this->callbackLock); + LG_LOCK(this->stateLock); + this->videoStatusFn = NULL; + this->videoStatusOpaque = NULL; + LG_UNLOCK(this->stateLock); + LG_UNLOCK(this->callbackLock); + + networkConnectionDestroy(&this->connection); + lgFreeEvent(this->frameEvent); + lgFreeEvent(this->promptIdle); + networkKnownHostsFree(&this->knownHosts); + LG_LOCK_FREE(this->promptLock); + LG_LOCK_FREE(this->stateLock); + LG_LOCK_FREE(this->callbackLock); + free(this); + *transport = NULL; +} + +static LG_TransportStatus networkConnectCancellable(LG_Transport * this, + LG_TransportSession * session, LG_TransportCancelledFn cancelled, + void * opaque) +{ + return networkConnectionConnect( + this->connection, session, cancelled, opaque); +} + +static LG_TransportStatus networkConnect(LG_Transport * this, + LG_TransportSession * session) +{ + return networkConnectCancellable(this, session, NULL, NULL); +} + +static void networkDisconnect(LG_Transport * this) +{ + networkConnectionDisconnect(this->connection); + lgSignalEvent(this->frameEvent); +} + +static bool networkSessionValid(LG_Transport * this) +{ + return networkConnectionValid(this->connection); +} + +static void networkSetStatusListener(LG_Transport * this, + LG_VideoStatusFn callback, void * opaque) +{ + LG_VideoStatus status; + + LG_LOCK(this->callbackLock); + LG_LOCK(this->stateLock); + this->videoStatusFn = callback; + this->videoStatusOpaque = opaque; + status = this->videoStatus; + LG_UNLOCK(this->stateLock); + + if (callback) + callback(opaque, &status); + LG_UNLOCK(this->callbackLock); +} + +static bool networkSupportsDMA(LG_Transport * this) +{ + (void)this; + return false; +} + +static bool networkAttachRenderer(LG_Transport * this, + const LG_RendererInterop * interop) +{ + const size_t requiredSize = offsetof(LG_RendererInterop, + egl.sharedTextureImport) + sizeof(interop->egl.sharedTextureImport); + if (!interop || + interop->version < LG_RENDERER_INTEROP_EGL_TEXTURE_VERSION || + interop->size < requiredSize || + interop->type != LG_RENDERER_INTEROP_EGL || + !interop->egl.display || !interop->egl.config || + !interop->egl.shareContext || !interop->egl.sharedTextureImport) + { + DEBUG_ERROR("The network transport requires EGL shared textures"); + return false; + } + + this->rendererAttached = true; + return true; +} + +static void networkDetachRenderer(LG_Transport * this) +{ + this->rendererAttached = false; +} + +static LG_TransportStatus networkNextFrame(LG_Transport * this, bool useDMA, + LG_TransportFrame * frame) +{ + (void)useDMA; + (void)frame; + + if (!this->rendererAttached) + return LG_TRANSPORT_ERROR; + if (!networkSessionValid(this)) + return LG_TRANSPORT_DISCONNECTED; + + lgWaitEvent(this->frameEvent, 100); + return networkSessionValid(this) ? + LG_TRANSPORT_UNAVAILABLE : LG_TRANSPORT_DISCONNECTED; +} + +static void networkGetFrameTiming(LG_Transport * this, + const LG_TransportFrame * frame, LG_TransportFrameTiming * timing) +{ + (void)this; + (void)frame; + memset(timing, 0, sizeof(*timing)); +} + +static void networkReleaseFrame(LG_Transport * this, + LG_TransportFrame * frame) +{ + (void)this; + (void)frame; +} + +static void networkCancelFrameWait(LG_Transport * this) +{ + lgSignalEvent(this->frameEvent); +} + +static void networkStopFrame(LG_Transport * this) +{ + lgSignalEvent(this->frameEvent); +} + +static const LG_FrameOps networkFrameOps = +{ + .setStatusListener = networkSetStatusListener, + .supportsDMA = networkSupportsDMA, + .attachRenderer = networkAttachRenderer, + .detachRenderer = networkDetachRenderer, + .nextFrame = networkNextFrame, + .getFrameTiming = networkGetFrameTiming, + .releaseFrame = networkReleaseFrame, + .cancelFrameWait = networkCancelFrameWait, + .stopFrame = networkStopFrame, +}; + +static const LG_VideoOps networkVideoOps = +{ + .name = "network", + .type = LG_VIDEO_TYPE_FRAME, + .frame = &networkFrameOps, +}; + +static const LG_VideoOps * networkGetVideoOps(LG_Transport * this) +{ + (void)this; + return &networkVideoOps; +} + +static const LG_InputOps * networkGetInputOps(LG_Transport * this, + void ** opaque) +{ + (void)this; + *opaque = NULL; + return NULL; +} + +static const LG_AudioOps * networkGetAudioOps(LG_Transport * this, + void ** opaque) +{ + (void)this; + *opaque = NULL; + return NULL; +} + +static const LG_ClipboardOps * networkGetClipboardOps(LG_Transport * this, + void ** opaque) +{ + (void)this; + *opaque = NULL; + return NULL; +} + +static LG_TransportStatus networkSendControl(LG_Transport * this, + const LG_TransportControl * control, LG_TransportControlToken * token) +{ + (void)control; + (void)token; + return networkSessionValid(this) ? + LG_TRANSPORT_UNAVAILABLE : LG_TRANSPORT_DISCONNECTED; +} + +static LG_TransportStatus networkControlStatus(LG_Transport * this, + LG_TransportControlToken token) +{ + (void)token; + return networkSessionValid(this) ? + LG_TRANSPORT_UNAVAILABLE : LG_TRANSPORT_DISCONNECTED; +} + +const LG_TransportOps LGT_Network = +{ + .name = "network", + .setup = networkSetup, + .create = networkCreate, + .destroy = networkDestroy, + .connect = networkConnect, + .connectCancellable = networkConnectCancellable, + .disconnect = networkDisconnect, + .sessionValid = networkSessionValid, + .getVideoOps = networkGetVideoOps, + .getInputOps = networkGetInputOps, + .getAudioOps = networkGetAudioOps, + .getClipboardOps = networkGetClipboardOps, + .sendControl = networkSendControl, + .controlStatus = networkControlStatus, +}; diff --git a/client/transports/Network/network.h b/client/transports/Network/network.h new file mode 100644 index 00000000..f7c3cff6 --- /dev/null +++ b/client/transports/Network/network.h @@ -0,0 +1,25 @@ +/** + * 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 "interface/transport.h" + +extern const LG_TransportOps LGT_Network; diff --git a/client/transports/Network/platform.h b/client/transports/Network/platform.h new file mode 100644 index 00000000..29174585 --- /dev/null +++ b/client/transports/Network/platform.h @@ -0,0 +1,25 @@ +/** + * 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 + +bool networkPlatformReplaceFile(const char * source, const char * target); diff --git a/client/transports/Network/platform/linux/fingerprint.c b/client/transports/Network/platform/linux/fingerprint.c new file mode 100644 index 00000000..bd5669c0 --- /dev/null +++ b/client/transports/Network/platform/linux/fingerprint.c @@ -0,0 +1,40 @@ +/** + * 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 "../../fingerprint.h" + +#include + +bool networkFingerprintSHA256(const uint8_t * data, size_t size, + uint8_t fingerprint[NETWORK_FINGERPRINT_SIZE]) +{ + EVP_MD_CTX * context = EVP_MD_CTX_new(); + if (!context) + return false; + + unsigned int outputSize = 0; + const bool result = + EVP_DigestInit_ex(context, EVP_sha256(), NULL) == 1 && + EVP_DigestUpdate(context, data, size) == 1 && + EVP_DigestFinal_ex(context, fingerprint, &outputSize) == 1 && + outputSize == NETWORK_FINGERPRINT_SIZE; + EVP_MD_CTX_free(context); + return result; +} diff --git a/client/transports/Network/platform/linux/platform.c b/client/transports/Network/platform/linux/platform.c new file mode 100644 index 00000000..60284450 --- /dev/null +++ b/client/transports/Network/platform/linux/platform.c @@ -0,0 +1,28 @@ +/** + * 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 "../../platform.h" + +#include + +bool networkPlatformReplaceFile(const char * source, const char * target) +{ + return rename(source, target) == 0; +} diff --git a/client/transports/Network/platform/windows/fingerprint.c b/client/transports/Network/platform/windows/fingerprint.c new file mode 100644 index 00000000..043fcb5b --- /dev/null +++ b/client/transports/Network/platform/windows/fingerprint.c @@ -0,0 +1,39 @@ +/** + * 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 "../../fingerprint.h" + +#include +#include +#include + +#include + +bool networkFingerprintSHA256(const uint8_t * data, size_t size, + uint8_t fingerprint[NETWORK_FINGERPRINT_SIZE]) +{ + if (size > ULONG_MAX) + return false; + + DWORD outputSize = NETWORK_FINGERPRINT_SIZE; + return CryptHashCertificate2(BCRYPT_SHA256_ALGORITHM, 0, NULL, + data, (DWORD)size, fingerprint, &outputSize) && + outputSize == NETWORK_FINGERPRINT_SIZE; +} diff --git a/client/transports/Network/platform/windows/platform.c b/client/transports/Network/platform/windows/platform.c new file mode 100644 index 00000000..fcfbc255 --- /dev/null +++ b/client/transports/Network/platform/windows/platform.c @@ -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 + */ + +#include "../../platform.h" + +#include "common/win32.h" + +#include +#include + +bool networkPlatformReplaceFile(const char * source, const char * target) +{ + wchar_t * sourceWide = lgUtf8ToWide(source); + wchar_t * targetWide = lgUtf8ToWide(target); + if (!sourceWide || !targetWide) + { + free(targetWide); + free(sourceWide); + return false; + } + + const bool result = MoveFileExW(sourceWide, targetWide, + MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH); + free(targetWide); + free(sourceWide); + return result; +}