From 63c19a81774f7822f7fdc464213ea3fe5626041f Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 20 Jul 2026 02:44:28 +0100 Subject: [PATCH] Persist Aetheria QUIC transport identity --- .gitignore | 1 + Aetheria.State.Daemon/Program.cs | 86 +++++++++++++++++++++++-- docs/aetheria-current-codebase-model.md | 10 +++ 3 files changed, 92 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 20a0dd7b9..6c92ebc09 100644 --- a/.gitignore +++ b/.gitignore @@ -133,6 +133,7 @@ Aetheria.State.AuthoritySmoke/obj/ Aetheria.State.Freeze/bin/ Aetheria.State.Freeze/obj/ Aetheria.State.Daemon/*.log +*.client-quic-*.pfx # JetBrains Rider .idea folder .idea/ diff --git a/Aetheria.State.Daemon/Program.cs b/Aetheria.State.Daemon/Program.cs index 81e3fcb16..76a204ac5 100644 --- a/Aetheria.State.Daemon/Program.cs +++ b/Aetheria.State.Daemon/Program.cs @@ -763,17 +763,34 @@ static async Task StartClientCultMeshHostAsync( AetheriaUnityBundleArtifactSet unityBundles, Func latestFrame) { + var traceStartup = string.Equals( + Environment.GetEnvironmentVariable("AETHERIA_TRACE_STARTUP_PHASES"), + "1", + StringComparison.Ordinal); + var startupPhase = Stopwatch.StartNew(); + void Trace(string phase) + { + if (traceStartup) + Console.WriteLine($"Aetheria client-host phase {phase} took {startupPhase.Elapsed.TotalMilliseconds:0.###}ms."); + startupPhase.Restart(); + } + var contentCache = new CultCache(CultMesh.CreateCultCacheDocumentRegistry( typeof(CultMeshCdnArtifactManifest), typeof(CultMeshCdnArtifactChunk))); var bundleCdnDocuments = BuildBundleCdnDocuments(node, contentCache, unityBundles, options.DaemonId); + Trace("cdn-documents"); var server = new TcpFramedCultNetSchemaServer(new TcpListener( ParseBindAddress(options.ClientCultMeshHost), options.ClientCultMeshPort)); var content = new CultMeshTcpContentServer(new TcpListener( ParseBindAddress(options.ClientCultMeshHost), options.ClientCultMeshContentPort), contentCache); - var realtimeCertificate = CreateRealtimeCertificate(options.ClientCultMeshAdvertiseHost); + Trace("tcp-listeners"); + var realtimeCertificate = LoadOrCreateRealtimeCertificate( + options.ClientCultMeshAdvertiseHost, + options.ClientCultMeshCertificatePath); + Trace("realtime-certificate"); CultMeshQuicRealtimeServer realtime; try { @@ -784,6 +801,7 @@ static async Task StartClientCultMeshHostAsync( options.ClientCultMeshQuicPort), ServerCertificate = realtimeCertificate }).ConfigureAwait(false); + Trace("quic-listen"); } catch { @@ -1279,7 +1297,53 @@ static X509Certificate2 CreateRealtimeCertificate(string advertisedHost) using var generated = request.CreateSelfSigned( DateTimeOffset.UtcNow.AddMinutes(-1), DateTimeOffset.UtcNow.AddDays(7)); - return X509CertificateLoader.LoadPkcs12(generated.Export(X509ContentType.Pfx), null); + return X509CertificateLoader.LoadPkcs12( + generated.Export(X509ContentType.Pfx), + null, + X509KeyStorageFlags.Exportable | X509KeyStorageFlags.EphemeralKeySet); +} + +static X509Certificate2 LoadOrCreateRealtimeCertificate(string advertisedHost, string certificatePath) +{ + if (string.IsNullOrWhiteSpace(certificatePath)) + throw new ArgumentException("Realtime certificate path must be non-empty.", nameof(certificatePath)); + + if (File.Exists(certificatePath)) + { + try + { + var existing = X509CertificateLoader.LoadPkcs12FromFile( + certificatePath, + null, + X509KeyStorageFlags.EphemeralKeySet); + if (existing.HasPrivateKey && + existing.NotAfter.ToUniversalTime() > DateTime.UtcNow.AddDays(1) && + string.Equals( + existing.GetNameInfo(X509NameType.DnsName, forIssuer: false), + advertisedHost, + StringComparison.OrdinalIgnoreCase)) + return existing; + existing.Dispose(); + } + catch (CryptographicException) + { + // Replace an unreadable or obsolete development certificate below. + } + } + + var generated = CreateRealtimeCertificate(advertisedHost); + Directory.CreateDirectory(Path.GetDirectoryName(certificatePath) ?? "."); + var temporaryPath = certificatePath + ".tmp-" + Guid.NewGuid().ToString("N"); + try + { + File.WriteAllBytes(temporaryPath, generated.Export(X509ContentType.Pfx)); + File.Move(temporaryPath, certificatePath, overwrite: true); + } + finally + { + if (File.Exists(temporaryPath)) File.Delete(temporaryPath); + } + return generated; } static void InjectCultMeshCdnManifestSnapshots( @@ -3828,6 +3892,7 @@ internal sealed class AetheriaDaemonHostOptions public int ClientCultMeshPort { get; init; } = 3076; public int ClientCultMeshContentPort { get; init; } public int ClientCultMeshQuicPort { get; init; } + public string ClientCultMeshCertificatePath { get; init; } = ""; public string AetheriaResourcesRoot { get; init; } = ""; public string AssetBundleRoot { get; init; } = ""; public IReadOnlyList PeerCultMeshEndpoints { get; init; } = Array.Empty(); @@ -3862,6 +3927,7 @@ internal sealed class AetheriaDaemonHostOptions var clientCultMeshPort = ReadNonNegativeInt(args, "--client-cultmesh-port") ?? 3076; var clientCultMeshContentPort = ReadNonNegativeInt(args, "--client-cultmesh-content-port") ?? 0; var clientCultMeshQuicPort = ReadNonNegativeInt(args, "--client-cultmesh-quic-port") ?? 0; + var clientCultMeshCertificatePath = ReadOption(args, "--client-cultmesh-certificate-path"); var aetheriaResourcesRoot = ReadOption(args, "--aetheria-resources-root"); var assetBundleRoot = ReadOption(args, "--asset-bundle-root"); RejectRemovedOption(args, "--rts-cultmesh-port", "--client-cultmesh-port"); @@ -3876,11 +3942,18 @@ internal sealed class AetheriaDaemonHostOptions var requestedTerminusScenario = ReadOption(args, "--terminus-scenario"); var terminusScenario = AetheriaDaemonTerminusScenarios.Parse(requestedTerminusScenario); + var resolvedStatePath = string.IsNullOrWhiteSpace(state) + ? AetheriaStatePaths.ResolveDefaultStatePath(root) + : Path.GetFullPath(state); + var certificateHost = string.IsNullOrWhiteSpace(clientCultMeshAdvertiseHost) + ? (string.IsNullOrWhiteSpace(clientCultMeshHost) || clientCultMeshHost == "0.0.0.0" || clientCultMeshHost == "*" ? "127.0.0.1" : clientCultMeshHost) + : clientCultMeshAdvertiseHost; + var certificateHostToken = Convert.ToHexString(SHA256.HashData( + System.Text.Encoding.UTF8.GetBytes(certificateHost))).Substring(0, 12).ToLowerInvariant(); + return new AetheriaDaemonHostOptions { - StatePath = string.IsNullOrWhiteSpace(state) - ? AetheriaStatePaths.ResolveDefaultStatePath(root) - : Path.GetFullPath(state), + StatePath = resolvedStatePath, DaemonId = string.IsNullOrWhiteSpace(daemonId) ? "aetheria-daemon" : daemonId, SessionId = string.IsNullOrWhiteSpace(sessionId) ? "local" : sessionId, VerseId = string.IsNullOrWhiteSpace(verseId) ? "aetheria.local" : verseId, @@ -3894,6 +3967,9 @@ internal sealed class AetheriaDaemonHostOptions ClientCultMeshPort = clientCultMeshPort, ClientCultMeshContentPort = clientCultMeshContentPort, ClientCultMeshQuicPort = clientCultMeshQuicPort, + ClientCultMeshCertificatePath = string.IsNullOrWhiteSpace(clientCultMeshCertificatePath) + ? resolvedStatePath + $".client-quic-{certificateHostToken}.pfx" + : Path.GetFullPath(clientCultMeshCertificatePath), AetheriaResourcesRoot = string.IsNullOrWhiteSpace(aetheriaResourcesRoot) ? Path.GetFullPath(Path.Combine(root, "Assets", "Resources")) : Path.GetFullPath(aetheriaResourcesRoot), diff --git a/docs/aetheria-current-codebase-model.md b/docs/aetheria-current-codebase-model.md index 260ac426d..53b6fd49e 100644 --- a/docs/aetheria-current-codebase-model.md +++ b/docs/aetheria-current-codebase-model.md @@ -154,6 +154,16 @@ smuggled into filtered hydration. demand and recover the durable value. On the Aetheria restart artifact the initial flush fell from 115–140 ms to 3.0–3.2 ms. +The direct client QUIC listener uses a stable daemon-service certificate stored +beside the state file by default; `--client-cultmesh-certificate-path` selects a +provisioned location. The advertised-host hash is part of the default filename, +and the daemon replaces certificates that are unreadable, lack a private key, +target another host, or are within one day of expiry. This transport identity +does not own Eve provider or body-producer identity. On the restart witness, +certificate setup fell from 153 ms of RSA generation to 27 ms of PFX loading, +and total client-host startup fell from about 319 ms to 183 ms. The first boot +still pays certificate creation once. + ### Restored-frame client readiness - **Owner:** the latest hard-flushed daemon frame owns restart world truth. Its