Files
TeslaSuite/Contract/Client/PodManagerConnection.cs
T
CydandClaude Opus 4.8 b9d8027cf6 Extract shared contract, drop BinaryFormatter wire, modernize to net8/x64
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>
2026-06-30 08:15:17 -05:00

277 lines
10 KiB
C#

// =============================================================================
// Tesla.Contract — Console-side RPC client (net48 only)
// =============================================================================
// Opens an OFB-encrypted TCP connection to the pod (port 53290) and dispatches
// ILauncherService calls as framed JSON RpcRequest / RpcResponse pairs (see
// PodRpcProtocol.cs). Replaces the original serialized-MethodBase + BinaryFormatter
// scheme; the OFB transport (PodConfigurationServer.NegotiateCryptoStreams) is
// unchanged.
//
// Depends on Tesla.PodConfigurationServer (Tesla.SecureConfig) for the crypto
// handshake, so it is compiled for net48 only. The Launcher is the server end.
// =============================================================================
using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Reflection;
using System.Text.Json;
using System.Threading;
namespace Tesla.Net
{
public class PodManagerConnection : IPodManagerConnection, ILauncherService, IDisposable
{
private TcpClient mOpenClient;
private object mCommSyncRoot = new object();
protected Stream mUnderlyingOutStream;
private BufferedStream mReader;
private BufferedStream mWriter;
private IPEndPoint mEndPoint;
private byte[] mKey;
public bool IsOpen
{
get
{
if (mOpenClient == null || mOpenClient.Client == null || mWriter == null || mReader == null)
{
return false;
}
return mOpenClient.Connected;
}
}
protected object SyncRoot => mCommSyncRoot;
public float VolumeLevel
{
get => Invoke<float>("get_VolumeLevel");
set => InvokeVoid("set_VolumeLevel", value);
}
public void Open(IPEndPoint remoteEndPoint, byte[] secureKey)
{
mEndPoint = remoteEndPoint;
mKey = secureKey;
mOpenClient = new TcpClient();
mOpenClient.SendTimeout = 10000;
mOpenClient.ReceiveTimeout = 10000;
mOpenClient.Connect(remoteEndPoint);
if (!PodConfigurationServer.NegotiateCryptoStreams(mOpenClient.GetStream(), secureKey, out var outStream, out var inStream))
{
mOpenClient.Close();
throw new IOException("Error connecting to pod.");
}
mUnderlyingOutStream = outStream;
mWriter = new BufferedStream(outStream);
mReader = new BufferedStream(inStream);
}
public void Close()
{
try
{
mWriter.Close();
mReader.Close();
mOpenClient.Client.Close();
mOpenClient = null;
}
catch
{
}
}
public Guid InstallProduct(string filepath)
{
if (!File.Exists(filepath))
{
return Guid.Empty;
}
FileStream outOfBandData = File.OpenRead(filepath);
Guid result = Guid.Empty;
try
{
result = OutOfBandInvocation.Invoke(mEndPoint, mKey, "InitiateInstallProduct", new object[0], outOfBandData);
return result;
}
catch (IOException)
{
return result;
}
}
// ── RPC core ─────────────────────────────────────────────────────────
// Writes a framed JSON request and (optionally) reads the framed response,
// deserializing its Result into the caller's expected type.
protected object InvokeCore(string method, bool waitForResponse, Type resultType, object[] parameters)
{
try
{
lock (mCommSyncRoot)
{
PodRpc.WriteRequest(mWriter, method, parameters);
if (!waitForResponse)
{
return null;
}
RpcResponse response = PodRpc.ReadResponse(mReader);
if (response.Error != null)
{
throw new Exception("Server function threw an exception: " + response.Error);
}
if (resultType == null
|| response.Result.ValueKind == JsonValueKind.Null
|| response.Result.ValueKind == JsonValueKind.Undefined)
{
return resultType != null && resultType.IsValueType
? Activator.CreateInstance(resultType)
: null;
}
return response.Result.Deserialize(resultType, PodRpc.JsonOptions);
}
}
catch (IOException innerException)
{
throw new TeslaConsole.ConnectionLostException("Connection Lost", innerException);
}
}
protected T Invoke<T>(string method, params object[] parameters)
=> (T)InvokeCore(method, waitForResponse: true, typeof(T), parameters);
protected void InvokeVoid(string method, params object[] parameters)
=> InvokeCore(method, waitForResponse: true, null, parameters);
protected void InvokeOneWay(string method, params object[] parameters)
=> InvokeCore(method, waitForResponse: false, null, parameters);
// ── ILauncherService ─────────────────────────────────────────────────
public void ClearStore() => InvokeOneWay("ClearStore");
public DateTime Ping(DateTime now) => Invoke<DateTime>("Ping", now);
public int LaunchApp(Guid launchKey) => Invoke<int>("LaunchApp", launchKey);
public LaunchPair[] GetLaunchableApps() => Invoke<LaunchPair[]>("GetLaunchableApps");
public Guid InitiateInstallProduct() => Invoke<Guid>("InitiateInstallProduct");
public OutOfBandProgress GetOutOfBandProgress(Guid installId)
=> Invoke<OutOfBandProgress>("GetOutOfBandProgress", installId);
public void KillApp(Guid launchKey, int processId)
=> InvokeVoid("KillApp", launchKey, processId);
public void KillAllOfType(Guid launchKey) => InvokeVoid("KillAllOfType", launchKey);
public void KillAllApps() => InvokeVoid("KillAllApps");
public void Shutdown(bool doRestart) => InvokeVoid("Shutdown", doRestart);
public LaunchedAppData[] GetLaunchedApps() => Invoke<LaunchedAppData[]>("GetLaunchedApps");
public LaunchData[] GetInstalledApps() => Invoke<LaunchData[]>("GetInstalledApps");
public void RemoveApp(Guid index) => InvokeVoid("RemoveApp", index);
public void InstallApp(LaunchData data) => InvokeVoid("InstallApp", data);
public void UninstallApp(Guid launchKey) => InvokeVoid("UninstallApp", launchKey);
public FullUpdateData FullUpdate() => Invoke<FullUpdateData>("FullUpdate");
public void Dispose()
{
Close();
}
}
public class OutOfBandInvocation : PodManagerConnection
{
private Stream mOutOfBandData;
public static Guid Invoke(IPEndPoint target, byte[] key, string methodName, object[] parameters, Stream outOfBandData)
{
return Invoke(target, key, methodName, parameters, outOfBandData, null);
}
public static Guid Invoke(IPEndPoint target, byte[] key, MethodBase method, object[] parameters, Stream outofBandData)
{
return Invoke(target, key, method.Name, parameters, outofBandData, null);
}
public static Guid Invoke(IPEndPoint target, byte[] key, MethodBase method, object[] parameters, Stream outOfBandData, OutOfBandProgressChanged progressCallback)
{
return Invoke(target, key, method.Name, parameters, outOfBandData, progressCallback);
}
public static Guid Invoke(IPEndPoint target, byte[] key, string methodName, object[] parameters, Stream outOfBandData, OutOfBandProgressChanged progressCallback)
{
OutOfBandInvocation outOfBandInvocation = new OutOfBandInvocation(target, key);
// progressCallback is vestigial: install progress is polled via
// GetOutOfBandProgress, never pushed. Kept on the signature for surface parity.
_ = progressCallback;
if (outOfBandData != null)
{
outOfBandInvocation.mOutOfBandData = outOfBandData;
}
Guid result = outOfBandInvocation.Invoke<Guid>(methodName, parameters ?? new object[0]);
ThreadPool.QueueUserWorkItem(AppendOutOfBandData, outOfBandInvocation);
return result;
}
protected static void AppendOutOfBandData(object state)
{
OutOfBandInvocation outOfBandInvocation = (OutOfBandInvocation)state;
using (outOfBandInvocation)
{
try
{
outOfBandInvocation.WriteOutOfBandData();
}
catch (IOException)
{
}
}
}
protected OutOfBandInvocation(IPEndPoint target, byte[] key)
{
Open(target, key);
}
protected void WriteOutOfBandData()
{
lock (base.SyncRoot)
{
int num = 8192;
byte[] buffer = new byte[num];
long num2 = mOutOfBandData.Length;
mUnderlyingOutStream.Write(BitConverter.GetBytes(num2), 0, 8);
while (num2 > num)
{
mOutOfBandData.Read(buffer, 0, num);
mUnderlyingOutStream.Write(buffer, 0, num);
num2 -= num;
}
if (num2 > 0)
{
mOutOfBandData.Read(buffer, 0, (int)num2);
mUnderlyingOutStream.Write(buffer, 0, (int)num2);
}
mUnderlyingOutStream.Flush();
}
}
}
}