// =============================================================================
// TeslaLauncher — Windows Service (Session 0)
// =============================================================================
// Replaces TeslaLauncherService.exe (Elsewhen Studios LLC, .NET Framework 2.0).
// Runs in Session 0, auto-starts at boot before user login.
// Listens on TCP 53290 for OFB-encrypted framed-JSON RPC from TeslaConsole.
// Forwards commands to TeslaLauncherAgent (user session) via Named Pipe.
//
// On first boot (DHCP detected), runs SecureConfig to receive network
// configuration from the Console and establish the session key.
// =============================================================================
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.IO.Pipes;
using System.Net;
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using TeslaSecureConfig;
using Tesla.Launcher.Shared;
using Tesla.Net;
namespace Tesla.Launcher.Service
{
public class TeslaLauncherService : BackgroundService
{
// ---------------------------------------------------------------
// CONFIGURATION
// ---------------------------------------------------------------
///
/// TCP port the Console connects to for RPC commands.
/// Original: ManagePort = 53290 (confirmed from decompiled TeslaLauncherService.exe).
/// Each connection is wrapped in OFB encryption via NegotiateCryptoStreams
/// using the session key from SecureConfig (TeslaKeyStore.key).
///
private const int CONSOLE_PORT = 53290;
///
/// Named pipe connecting this service to TeslaLauncherAgent.exe.
/// Must match PIPE_NAME in TeslaLauncherAgent.cs.
///
private const string PIPE_NAME = "TeslaLauncherIPC";
///
/// Seconds to wait for the Agent to connect on the Named Pipe.
/// If the Agent isn't running (user not yet logged in), we return an error.
///
private const int AGENT_CONNECT_TIMEOUT_MS = 3000;
// ---------------------------------------------------------------
/// Path to session key file (original KeyStore format).
private static readonly string KEY_FILE = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
"TeslaLauncher", "TeslaKeyStore.key");
private readonly ILogger _logger;
private TcpListener _listener;
private readonly List _clientTasks = new();
private readonly object _taskLock = new();
private byte[] _sessionKey;
// Install product — file transfer progress tracking.
// InitiateInstallProduct creates a Guid; the Console then streams the
// game archive on the SAME OFB connection. GetOutOfBandProgress polls
// this dictionary from a separate connection.
private readonly Dictionary _installProgress = new();
private readonly object _installLock = new();
public TeslaLauncherService(ILogger logger)
{
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation(
"Tesla Launcher Service starting on port {Port}", CONSOLE_PORT);
// ── Boot diagnostic — always written so every reboot is traceable ──
// Logs adapter state to podconf.log before IsMachineConfigured() decides,
// visible even if SecureConfig does not run.
var logPath = Path.Combine(AppContext.BaseDirectory, "podconf.log");
void StartupLog(string msg) => File.AppendAllText(logPath,
$"[{DateTime.Now:HH:mm:ss}] {msg}{Environment.NewLine}");
StartupLog("=== Service starting ===");
foreach (var nic in System.Net.NetworkInformation.NetworkInterface.GetAllNetworkInterfaces())
{
if (nic.NetworkInterfaceType != System.Net.NetworkInformation.NetworkInterfaceType.Ethernet) continue;
if (nic.OperationalStatus != System.Net.NetworkInformation.OperationalStatus.Up) continue;
try
{
var ipv4 = nic.GetIPProperties().GetIPv4Properties();
StartupLog($" NIC [{nic.Name}] DHCP={ipv4.IsDhcpEnabled} Virtual={IsVirtualAdapter(nic)}");
}
catch { StartupLog($" NIC [{nic.Name}] (no IPv4 props)"); }
}
// ── First-boot secure configuration ──────────────────────────────
// If the Ethernet adapter is still on DHCP the cockpit has not yet
// been configured. Run PodSecureConfigurator here in Session 0:
// • broadcasts the RQST beacon (MAC + RequestId)
// • displays passphrase on COM2 / PlasmaIO
// • receives UDP RPLY with AES-encrypted network configuration
// • applies netsh IP settings and hostname
// • completes TCP handshake with Console + RSA key exchange
// • saves 32-byte session key to TeslaKeyStore.key
// After SecureConfig, fall through to the normal command listener —
// the original Service does the same (ExecutionMode after Configure).
if (!IsMachineConfigured())
{
StartupLog("IsMachineConfigured=false → running SecureConfig");
_logger.LogInformation("Machine is unconfigured (DHCP). Running SecureConfiguration.");
await Task.Run(() => RunSecureConfiguration(stoppingToken), stoppingToken);
// Load key that was saved during SecureConfig
_sessionKey = LoadSessionKey();
if (_sessionKey == null)
{
_logger.LogWarning("SecureConfig completed but no session key was saved. Cannot start command listener.");
return;
}
StartupLog("SecureConfig complete → starting command listener on port 53290");
}
else
{
StartupLog("IsMachineConfigured=true → loading session key");
_sessionKey = LoadSessionKey();
if (_sessionKey == null)
{
_logger.LogWarning("Machine is configured but TeslaKeyStore.key not found. Reboot with DHCP to re-run SecureConfig.");
StartupLog("ERROR: TeslaKeyStore.key not found — cannot start command listener");
return;
}
StartupLog($"Session key loaded ({_sessionKey.Length} bytes) → starting normal service");
}
_logger.LogInformation("Starting command listener on port {Port}.", CONSOLE_PORT);
try
{
_listener = new TcpListener(IPAddress.Any, CONSOLE_PORT);
_listener.Start();
_logger.LogInformation("Listening. Waiting for Console connections...");
while (!stoppingToken.IsCancellationRequested)
{
try
{
var client = await _listener.AcceptTcpClientAsync(stoppingToken);
_logger.LogInformation(
"Console connected from {EP}", client.Client.RemoteEndPoint);
// Handle each Console connection on its own thread.
// Frame reads are synchronous, so we offload to a
// thread-pool thread via Task.Run.
var task = Task.Run(
() => HandleConsoleClient(client, stoppingToken),
stoppingToken);
lock (_taskLock)
{
_clientTasks.Add(task);
_clientTasks.RemoveAll(t => t.IsCompleted);
}
}
catch (OperationCanceledException) { break; }
catch (Exception ex)
{
_logger.LogError(ex, "Error accepting Console connection");
}
}
}
finally
{
_listener?.Stop();
_logger.LogInformation("Tesla Launcher Service stopped.");
}
}
// ── Console Connection Handler ───────────────────────────────────
private async Task HandleConsoleClient(TcpClient tcpClient, CancellationToken ct)
{
using var client = tcpClient;
using var netStream = client.GetStream();
netStream.WriteTimeout = 10_000;
netStream.ReadTimeout = 30_000;
// ── OFB crypto negotiation ────────────────────────────────────
// Both sides exchange 16-byte IVs, then verify "CONF" handshake.
// Read Console's IV
var consoleIv = new byte[16];
try { ReadExactSync(netStream, consoleIv); }
catch (Exception ex)
{
_logger.LogWarning("Failed reading Console IV: {Msg}", ex.Message);
return;
}
// Generate and send our IV
var podIv = new byte[16];
RandomNumberGenerator.Fill(podIv);
netStream.Write(podIv, 0, 16);
netStream.Flush();
// Create OFB duplex stream — lives for the entire session
using var ofb = new OFBDuplexStream(netStream, _sessionKey,
writeIv: consoleIv, // pod→console (other's IV)
readIv: podIv); // console→pod (own IV)
// CONF handshake — verify both sides have the same session key
var confMsg = Encoding.UTF8.GetBytes("CONF");
ofb.Write(confMsg, 0, confMsg.Length);
ofb.Flush();
var confReply = new byte[4];
try { ReadExactSync(ofb, confReply); }
catch (Exception ex)
{
_logger.LogWarning("CONF read failed: {Msg}", ex.Message);
return;
}
if (!confReply.AsSpan().SequenceEqual(confMsg))
{
_logger.LogWarning("CONF mismatch — session key mismatch, dropping connection.");
return;
}
Stream rpcStream = ofb;
var diagLog = Path.Combine(AppContext.BaseDirectory, "podconf.log");
_logger.LogInformation("Console session started.");
try
{
while (!ct.IsCancellationRequested && client.Connected)
{
RpcRequest request;
try
{
request = PodRpc.ReadRequest(rpcStream);
}
catch (EndOfStreamException)
{
_logger.LogInformation("Console disconnected (EOF).");
break;
}
catch (IOException ex)
{
_logger.LogInformation("Console disconnected (I/O: {Msg}).", ex.Message);
break;
}
catch (Exception ex)
{
_logger.LogWarning("Request read error ({Type}): {Msg}",
ex.GetType().Name, ex.Message);
File.AppendAllText(diagLog,
$"[{DateTime.Now:HH:mm:ss}] REQ ERROR: {ex}{Environment.NewLine}");
break;
}
if (request == null) break;
var funcName = request.Method ?? "???";
IReadOnlyList args = request.Args ?? new List();
_logger.LogInformation("Command: {Func}({ArgCount} args)", funcName, args.Count);
var start = DateTime.UtcNow;
object resultObj = null;
string error = null;
try
{
resultObj = await DispatchCommandAsync(funcName, args, ct);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error executing {Func}", funcName);
error = ex.Message;
File.AppendAllText(diagLog,
$"[{DateTime.Now:HH:mm:ss}] CMD {funcName} ERROR: {ex.Message}{Environment.NewLine}");
}
var dur = DateTime.UtcNow - start;
if (error == null)
{
File.AppendAllText(diagLog,
$"[{DateTime.Now:HH:mm:ss}] CMD {funcName} OK ({dur.TotalMilliseconds:F0}ms)" +
$"{(resultObj != null ? $" → {resultObj}" : "")}{Environment.NewLine}");
}
try
{
PodRpc.WriteResponse(rpcStream, resultObj, error);
}
catch (IOException)
{
_logger.LogInformation("Console disconnected while writing response.");
break;
}
catch (Exception serEx)
{
// Result type not JSON-serializable — send error instead.
_logger.LogWarning("Serialize failed for {Func}: {Msg}",
funcName, serEx.Message);
try { PodRpc.WriteResponse(rpcStream, null, $"Pod error: {serEx.Message}"); }
catch { break; }
}
// ── File receive after InitiateInstallProduct ────────────
// The Console streams the game archive on this same OFB
// connection immediately after receiving the Guid response:
// 8-byte Int64 file size + raw file bytes
// Then closes the connection (FIN).
if (funcName == "InitiateInstallProduct"
&& error == null
&& resultObj is Guid installGuid)
{
ReceiveInstallFile(rpcStream, installGuid, diagLog);
break; // connection done after file transfer
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Unhandled error in Console handler");
File.AppendAllText(diagLog,
$"[{DateTime.Now:HH:mm:ss}] HANDLER ERROR: {ex.GetType().Name}: {ex.Message}{Environment.NewLine}" +
$"[{DateTime.Now:HH:mm:ss}] Inner: {ex.InnerException?.Message}{Environment.NewLine}");
}
_logger.LogInformation("Console session ended.");
}
// ── Command Dispatch ─────────────────────────────────────────────
// Arguments arrive as JSON elements (RpcRequest.Args); each case extracts
// the typed values it needs. The returned object is serialized straight
// into the RpcResponse.
private async Task