// app/vpn/dragonrelaybackend.cpp #include "dragonrelaybackend.h" #include #include #include #include // Constructor / Destructor DragonRelayBackend::DragonRelayBackend(QObject *parent) : QObject(parent) , m_settings(QStringLiteral("WildDragon"), QStringLiteral("DragonMoonlight")) { // Wire RelayClient signals connect(&m_relay, &RelayClient::loginSucceeded, this, [this]() { onLoginDone(true, QString()); }); connect(&m_relay, &RelayClient::loginFailed, this, [this](const QString &err) { onLoginDone(false, err); }); connect(&m_relay, &RelayClient::vpnProvisioned, this, [this](const RelayVPNConf &conf) { onVPNPeerProvisioned(conf); }); connect(&m_relay, &RelayClient::vpnProvisionFailed, this, [this](const QString &err) { onVPNError(err); }); connect(&m_relay, &RelayClient::hostsReady, this, [this](const QList &hosts) { onHostsFetched(hosts); }); connect(&m_relay, &RelayClient::hostsFetchFailed, this, [this](const QString &err) { onHostsError(err); }); // Wire TunnelManager signals connect(&m_tunnel, &TunnelManager::tunnelUp, this, &DragonRelayBackend::onTunnelUp); connect(&m_tunnel, &TunnelManager::tunnelDown, this, &DragonRelayBackend::onTunnelDown); connect(&m_tunnel, &TunnelManager::tunnelError, this, &DragonRelayBackend::onTunnelError); // Host poll timer (every 30 s while tunnel is up) m_hostPollTimer.setInterval(30 * 1000); m_hostPollTimer.setSingleShot(false); connect(&m_hostPollTimer, &QTimer::timeout, this, &DragonRelayBackend::pollHosts); // Restore last-used connection settings so QML can pre-fill the form m_savedURL = m_settings.value(QStringLiteral("relay/url")).toString(); m_savedUser = m_settings.value(QStringLiteral("relay/username")).toString(); } DragonRelayBackend::~DragonRelayBackend() { m_hostPollTimer.stop(); m_tunnel.stop(); } void DragonRelayBackend::setStatus(Status s, const QString &text) { bool changed = (m_status != s) || (!text.isEmpty() && text != m_statusText); m_status = s; if (!text.isEmpty()) m_statusText = text; if (changed) emit statusChanged(); } void DragonRelayBackend::connectRelay(const QString &url, const QString &username, const QString &password) { if (m_status == Connecting || m_status == TunnelUp || m_status == Ready) return; m_savedURL = url; m_savedUser = username; m_savedPass = password; // Persist URL + username (NOT password - keychain integration is a TODO). m_settings.setValue(QStringLiteral("relay/url"), url); m_settings.setValue(QStringLiteral("relay/username"), username); setStatus(Connecting, QStringLiteral("Logging in...")); m_relay.login(QUrl(url), username, password); } void DragonRelayBackend::disconnectRelay() { stopHostPoll(); m_tunnel.stop(); // triggers onTunnelDown asynchronously m_relay.revokeVPN(); // fire-and-forget m_hosts.clear(); m_hostDisplays.clear(); m_tunnelIP.clear(); emit hostsChanged(); emit tunnelIPChanged(); setStatus(Disconnected, QStringLiteral("Disconnected")); } void DragonRelayBackend::streamHost(const QString &ip, const QString &app, int displayIndex) { if (m_status != Ready && m_status != TunnelUp) { qWarning() << "DragonRelayBackend::streamHost called while not ready"; return; } qDebug() << "DragonRelayBackend::streamHost ip=" << ip << "app=" << app << "displayIndex=" << displayIndex; QStringList args; args << QStringLiteral("stream") << ip << app; if (displayIndex > 0) { args << QStringLiteral("--display") << QString::number(displayIndex); } QProcess::startDetached(QStringLiteral("moonlight"), args); } QVariantList DragonRelayBackend::displaysForHost(const QString &hostIP) const { return m_hostDisplays.value(hostIP); } void DragonRelayBackend::streamHostDisplay(const QString &hostIP, int displayIndex) { qDebug() << "Streaming host" << hostIP << "display index" << displayIndex; const QVariantList displays = displaysForHost(hostIP); if (displayIndex < 0 || (!displays.isEmpty() && displayIndex >= displays.size())) { qWarning() << "DragonRelayBackend: displayIndex" << displayIndex << "out of bounds for host" << hostIP << "(has" << displays.size() << "displays)"; displayIndex = 0; // fall back to primary } streamHost(hostIP, QStringLiteral("Desktop"), displayIndex); } void DragonRelayBackend::refreshHosts() { if (m_status == TunnelUp || m_status == Ready) m_relay.fetchHosts(); } void DragonRelayBackend::pollHosts() { m_relay.fetchHosts(); } void DragonRelayBackend::startHostPoll() { m_relay.fetchHosts(); m_hostPollTimer.start(); } void DragonRelayBackend::stopHostPoll() { m_hostPollTimer.stop(); } void DragonRelayBackend::onLoginDone(bool ok, const QString &err) { if (!ok) { setStatus(Error, QStringLiteral("Login failed: ") + err); return; } setStatus(Connecting, QStringLiteral("Provisioning VPN peer...")); m_relay.provisionVPN(); } void DragonRelayBackend::onVPNPeerProvisioned(const RelayVPNConf &conf) { setStatus(Connecting, QStringLiteral("Starting WireGuard tunnel...")); WireGuardConfig cfg = WireGuardConfig::fromConf(conf.conf); if (!cfg.isValid()) { setStatus(Error, QStringLiteral( "Bad VPN config: missing PrivateKey, Address, PublicKey, or Endpoint")); m_relay.revokeVPN(); return; } if (!m_tunnel.start(cfg)) { // tunnel.start() emits tunnelError() synchronously on failure, // which routes through onTunnelError(). } } void DragonRelayBackend::onVPNError(const QString &err) { setStatus(Error, QStringLiteral("VPN error: ") + err); } void DragonRelayBackend::onHostsFetched(const QList &hosts) { m_hosts.clear(); m_hostDisplays.clear(); for (const auto &h : hosts) { QVariantMap m; m[QStringLiteral("name")] = h.name; m[QStringLiteral("ip")] = h.ip; m[QStringLiteral("port")] = h.port; m[QStringLiteral("online")] = true; m[QStringLiteral("source")] = QStringLiteral("relay"); m_hosts.append(m); m_hostDisplays[h.ip] = h.displays; } emit hostsChanged(); Status next = m_hosts.isEmpty() ? TunnelUp : Ready; if (m_status != next) { QString text = m_hosts.isEmpty() ? QStringLiteral("Tunnel up - no hosts yet") : QStringLiteral("Connected - %1 host(s)").arg(m_hosts.size()); setStatus(next, text); } } void DragonRelayBackend::onHostsError(const QString &err) { qWarning() << "DragonRelayBackend: hosts fetch error:" << err; // Don't downgrade to Error state - tunnel is still up. } void DragonRelayBackend::onTunnelUp(const QString &localIP) { if (m_tunnelIP == localIP && (m_status == TunnelUp || m_status == Ready)) return; // ignore duplicate signals (first-packet path + 1.5 s fallback timer) m_tunnelIP = localIP; emit tunnelIPChanged(); setStatus(TunnelUp, QStringLiteral("Tunnel up (%1) - fetching hosts...").arg(localIP)); startHostPoll(); } void DragonRelayBackend::onTunnelDown() { stopHostPoll(); m_tunnelIP.clear(); emit tunnelIPChanged(); if (m_status != Disconnected) setStatus(Disconnected, QStringLiteral("Tunnel closed")); } void DragonRelayBackend::onTunnelError(const QString &err) { stopHostPoll(); m_relay.revokeVPN(); setStatus(Error, QStringLiteral("Tunnel error: ") + err); }