Persist Aetheria QUIC transport identity
This commit is contained in:
parent
6fc92b3447
commit
63c19a8177
3 changed files with 92 additions and 5 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -763,17 +763,34 @@ static async Task<AetheriaClientCultMeshHost> StartClientCultMeshHostAsync(
|
|||
AetheriaUnityBundleArtifactSet unityBundles,
|
||||
Func<AetheriaRuntimeDaemonFrameDocument?> 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<AetheriaClientCultMeshHost> 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<string> PeerCultMeshEndpoints { get; init; } = Array.Empty<string>();
|
||||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue