Make the Console<->Launcher system source-built and modern now that the console is under our control and the WinXP-era pods are gone. Contract extraction (Contract/Tesla.Contract.csproj): - One multi-targeted (net48;net8.0-windows) source project for the RPC contract, replacing the vendored TeslaConsoleLaunchLib.dll and the hand-synced Tesla.Net replica in Launcher/LaunchModels_Shared.cs. Emits assembly TeslaConsoleLaunchLib. SecureConfig extraction (SecureConfig/Tesla.SecureConfig.csproj): - net48 source of the first-boot provisioning protocol (UDP beacons, OFB crypto, RSA key exchange), replacing the vendored TeslaSecureConfiguration.dll. Remove BinaryFormatter from the wire (RCE sink + the reason net6 was pinned): - Console<->Launcher RPC is now length-prefixed System.Text.Json frames (Contract/PodRpcProtocol.cs) over the unchanged OFB transport; dispatch by method name. Deleted the SerializationBinder / MethodInfoProxy machinery. - Console-local BinaryFormatter (Site config, mission replays) intentionally retained: local net48 file I/O, not the network surface. Runtime modernization: - Launcher Service + Agent: net6 -> net8, win-x86 -> win-x64 (all pods are 64-bit Win10). Kept the SHA1-default PBKDF2 (Console key-derivation compat) with SYSLIB0041 suppressed and documented. Tests: differential suite now 73 green. Added SecureConfigCompatTests (OFB ciphertext byte-identical to the vendored DLL) and PodRpcProtocolTests (JSON round-trip of every request/response shape); removed the now-obsolete BinaryFormatter byte-identity guard. Build hygiene: per-project obj dirs (Launcher/Directory.Build.props) fix a NuGet restore collision between the two Launcher projects sharing one folder. NOT runtime-verified against a live pod. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
121 lines
5.5 KiB
C#
121 lines
5.5 KiB
C#
// =============================================================================
|
|
// Tesla.Contract — Pod RPC protocol (Console <-> Launcher Service)
|
|
// =============================================================================
|
|
// The framed, JSON-based replacement for the old OFB-encrypted BinaryFormatter
|
|
// RPC. Runs ON TOP of the same OFB-encrypted stream (NegotiateCryptoStreams is
|
|
// unchanged); only the per-message serialization changed.
|
|
//
|
|
// Wire format, after the OFB/CONF handshake:
|
|
// request : [4-byte little-endian length][UTF-8 JSON RpcRequest]
|
|
// response: [4-byte little-endian length][UTF-8 JSON RpcResponse]
|
|
// The InitiateInstallProduct out-of-band file transfer (8-byte length + raw
|
|
// bytes) is unchanged and still follows its response frame on the same stream.
|
|
//
|
|
// Dispatch is by method NAME (RpcRequest.Method) — the old serialized-MethodBase
|
|
// + SerializationBinder + MethodInfoProxy machinery is gone. Both ends share this
|
|
// one file, so the request/response shape cannot drift.
|
|
// =============================================================================
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Text.Json;
|
|
|
|
namespace Tesla.Net
|
|
{
|
|
/// <summary>One RPC call: a method name plus its arguments as JSON elements.</summary>
|
|
public sealed class RpcRequest
|
|
{
|
|
public string Method { get; set; }
|
|
public List<JsonElement> Args { get; set; }
|
|
}
|
|
|
|
/// <summary>One RPC result: the return value as JSON, or an error message.</summary>
|
|
public sealed class RpcResponse
|
|
{
|
|
public JsonElement Result { get; set; } // JsonValueKind.Null for void / null
|
|
public string Error { get; set; } // null on success
|
|
}
|
|
|
|
/// <summary>Framing + (de)serialization shared by the Console client and the
|
|
/// Launcher Service. Synchronous to match the existing stream usage.</summary>
|
|
public static class PodRpc
|
|
{
|
|
/// <summary>Upper bound on a single JSON frame (the bulk install payload is
|
|
/// streamed out-of-band, not framed), guarding against hostile lengths.</summary>
|
|
public const int MaxFrameBytes = 16 * 1024 * 1024;
|
|
|
|
public static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions
|
|
{
|
|
// The Tesla.Net wire types (LaunchData, LaunchPair, ...) expose public
|
|
// FIELDS, which System.Text.Json ignores unless this is set.
|
|
IncludeFields = true,
|
|
};
|
|
|
|
// ── Framing ──────────────────────────────────────────────────────────
|
|
|
|
public static void WriteFrame(Stream stream, byte[] payload)
|
|
{
|
|
if (payload.Length > MaxFrameBytes)
|
|
throw new IOException($"RPC frame too large ({payload.Length} bytes).");
|
|
var len = BitConverter.GetBytes(payload.Length); // little-endian on x86/x64
|
|
stream.Write(len, 0, 4);
|
|
stream.Write(payload, 0, payload.Length);
|
|
stream.Flush();
|
|
}
|
|
|
|
public static byte[] ReadFrame(Stream stream)
|
|
{
|
|
var lenBuf = ReadExact(stream, 4);
|
|
int len = BitConverter.ToInt32(lenBuf, 0);
|
|
if (len < 0 || len > MaxFrameBytes)
|
|
throw new IOException($"RPC frame length out of range ({len}).");
|
|
return ReadExact(stream, len);
|
|
}
|
|
|
|
private static byte[] ReadExact(Stream stream, int count)
|
|
{
|
|
var buf = new byte[count];
|
|
int off = 0;
|
|
while (off < count)
|
|
{
|
|
int n = stream.Read(buf, off, count - off);
|
|
if (n == 0) throw new EndOfStreamException("Connection closed mid-frame.");
|
|
off += n;
|
|
}
|
|
return buf;
|
|
}
|
|
|
|
// ── Request ──────────────────────────────────────────────────────────
|
|
|
|
public static void WriteRequest(Stream stream, string method, object[] args)
|
|
{
|
|
var req = new RpcRequest { Method = method, Args = new List<JsonElement>() };
|
|
if (args != null)
|
|
foreach (var a in args)
|
|
req.Args.Add(JsonSerializer.SerializeToElement(a, JsonOptions));
|
|
WriteFrame(stream, JsonSerializer.SerializeToUtf8Bytes(req, JsonOptions));
|
|
}
|
|
|
|
public static RpcRequest ReadRequest(Stream stream)
|
|
=> JsonSerializer.Deserialize<RpcRequest>(ReadFrame(stream), JsonOptions);
|
|
|
|
// ── Response ─────────────────────────────────────────────────────────
|
|
|
|
public static void WriteResponse(Stream stream, object result, string error)
|
|
{
|
|
var resp = new RpcResponse
|
|
{
|
|
// Always a valid element (JSON null when there is no result): a
|
|
// default(JsonElement) is ValueKind.Undefined and is not serializable.
|
|
Result = JsonSerializer.SerializeToElement(error == null ? result : null, JsonOptions),
|
|
Error = error,
|
|
};
|
|
WriteFrame(stream, JsonSerializer.SerializeToUtf8Bytes(resp, JsonOptions));
|
|
}
|
|
|
|
public static RpcResponse ReadResponse(Stream stream)
|
|
=> JsonSerializer.Deserialize<RpcResponse>(ReadFrame(stream), JsonOptions);
|
|
}
|
|
}
|