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:
+36
-24
@@ -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 ---
|
||||
|
||||
Reference in New Issue
Block a user