XP11: single net40 binary runs on XP SP3 through Windows 11

Merge TeslaLauncherService (Session 0) + TeslaLauncherAgent (tray) into one
userland TeslaLauncher.exe. The split existed only to work around Vista+
Session 0 isolation; running everything in the auto-logged-in admin session
needs no service, no named pipe, and no flat<->wire conversion layer. The
original Elsewhen software was likewise a single binary.

net40 is the newest framework XP SP3 can install, and net40 assemblies load
in-place on the 4.8 runtime in Win10/11 -- one exe covers both.

- Contract: multi-target net48;net40. The net40 leg of PodRpcProtocol uses
  Newtonsoft.Json (STJ has no net40 target); JSON is shape-identical on the
  wire, and the request reader keeps date strings raw so Ping echoes
  byte-identically. Console keeps the untouched net48/STJ leg.
- MiniZip.cs: central-directory ZIP extractor (stored/deflate/ZIP64) since
  net40 has no ZipFile; zip-slip guarded.
- SecureConfig: RSACryptoServiceProvider instead of RSA.Create (net46+),
  no leaveOpen BinaryWriter/Reader, struct instead of ValueTuple, and no
  SetCompatibleTextRenderingDefault on the passcode thread (the merged app
  already has a form). netsh "interface ip" syntax was already XP-correct.
- Volume: nircmd -> CoreAudio (Vista+) -> winmm waveOutSetVolume (XP).
- Paths: CommonApplicationData resolved per-OS (XP has no C:\ProgramData);
  launcher log moved next to the key/config in the data dir.
- install.bat: dual-OS (cacls/icacls, netsh firewall/advfirewall, dism and
  UAC/notification steps skipped on XP, .NET 4.0 redist check, XP DHCP reset
  via netsh); no service registration -- HKLM Run key + auto-login on both;
  RegisterApplicationRestart supplies crash-restart on Vista+.
- Bench switches: /skipconfig, /port:NNNN.

Verified: Console + diff suite (103 tests) still green on the net48 leg;
E2E smoke test drove the net40 launcher over real TCP with the Console's own
PodManagerConnection (STJ client vs Newtonsoft server) -- Ping echo, volume,
app registry, launch/kill/watch, InstallProduct zip transfer + extract, and
uninstall orphan cleanup: 17/17 pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-10 18:59:06 -05:00
co-authored by Claude Fable 5
parent 1d95a800fd
commit 8730b9b966
16 changed files with 2090 additions and 2548 deletions
+36 -24
View File
@@ -416,7 +416,10 @@ namespace TeslaSecureConfig
}
else
{
(_adapterIndex, _adapterId, _adapterName) = FindFirstEthernetAdapter();
var adapter = FindFirstEthernetAdapter();
_adapterIndex = adapter.Index;
_adapterId = adapter.Id;
_adapterName = adapter.Name;
}
_log = logger ?? (s => Debug.WriteLine(s));
}
@@ -845,23 +848,23 @@ namespace TeslaSecureConfig
Log("Secure connection to console negotiated.");
// ── 4. RSA key exchange ─────────────────────────────────
using var rsa = RSA.Create(Proto.RsaKeySize);
// net40: RSA.Create(int) and RSAEncryptionPadding are net46+.
// RSACryptoServiceProvider.Decrypt(data, false) is the same
// PKCS#1 v1.5 padding, and works on XP SP3's CAPI.
using var rsa = new RSACryptoServiceProvider(Proto.RsaKeySize);
Log("Sending console final key.");
using (var bw = new BinaryWriter(ofb, Encoding.UTF8, leaveOpen: true))
{
bw.Write(rsa.ToXmlString(false)); // public key only
bw.Flush();
}
// net40 BinaryWriter/Reader have no leaveOpen overload; not
// disposing them keeps the OFB stream open, which is the intent.
var bw = new BinaryWriter(ofb, Encoding.UTF8);
bw.Write(rsa.ToXmlString(false)); // public key only
bw.Flush();
Log("Receiving console key.");
byte[] sessionKey;
using (var br = new BinaryReader(ofb, Encoding.UTF8, leaveOpen: true))
{
int encLen = br.ReadInt32();
byte[] enc = br.ReadBytes(encLen);
sessionKey = rsa.Decrypt(enc, RSAEncryptionPadding.Pkcs1);
}
var br = new BinaryReader(ofb, Encoding.UTF8);
int encLen = br.ReadInt32();
byte[] enc = br.ReadBytes(encLen);
byte[] sessionKey = rsa.Decrypt(enc, false); // PKCS#1 v1.5
Log($"Console key received ({sessionKey.Length} bytes).");
// Store session key — used for OFB on the management port (53290)
@@ -945,11 +948,12 @@ namespace TeslaSecureConfig
_plasma.WriteLine("Passphrase: {0}", _passphrase);
#if WINFORMS
// WinForms dialog — only shown when running as the Agent (user session)
// WinForms dialog on its own STA thread. Do NOT call
// SetCompatibleTextRenderingDefault here: the single-binary launcher
// already created its tray form, and calling it after any control
// exists throws InvalidOperationException.
var t = new Thread(() =>
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
_displayForm = new PasscodeDisplayForm(_requestId, _passphrase);
_displayForm.ShowDialog();
})
@@ -1129,10 +1133,18 @@ namespace TeslaSecureConfig
Log($"Warning: target IP {targetIp} not on any local subnet — keeping [{_adapterIndex}] {_adapterName}");
}
// Returns (IPv4 interface index, adapter GUID).
// Index is used with `netsh interface ipv4 ... interface=N` (avoids name-quoting issues).
// GUID is used for direct registry writes to persist static IP across reboots.
private static (int index, string id, string name) FindFirstEthernetAdapter()
// IPv4 interface index + adapter GUID + display name (a struct instead of a
// ValueTuple — net40 has no System.ValueTuple).
// Index avoids netsh name-quoting issues; GUID is used for direct registry
// writes to persist the static IP across reboots.
private struct AdapterInfo
{
public int Index;
public string Id;
public string Name;
}
private static AdapterInfo FindFirstEthernetAdapter()
{
foreach (var nic in NetworkInterface.GetAllNetworkInterfaces())
{
@@ -1150,7 +1162,7 @@ namespace TeslaSecureConfig
{
int idx = nic.GetIPProperties().GetIPv4Properties().Index;
Log2($"Selected physical adapter: [{idx}] {nic.Name} / {nic.Description}");
return (idx, nic.Id, nic.Name);
return new AdapterInfo { Index = idx, Id = nic.Id, Name = nic.Name };
}
catch (NetworkInformationException) { }
}
@@ -1165,7 +1177,7 @@ namespace TeslaSecureConfig
string.Equals(nic.Description, displayName, StringComparison.OrdinalIgnoreCase))
return nic.Id;
}
return FindFirstEthernetAdapter().id; // .name ignored in fallback
return FindFirstEthernetAdapter().Id; // Name ignored in fallback
}
// Find index for a named adapter (fallback when caller passes a display name)
@@ -1181,7 +1193,7 @@ namespace TeslaSecureConfig
}
}
// Fall back to auto-detect if name not matched
return FindFirstEthernetAdapter().index; // .name ignored in fallback
return FindFirstEthernetAdapter().Index; // Name ignored in fallback
}
// --- Random string generator ---