using System.Text.Json; using Fleet.Core; using Fleet.Rpc; using Fleet.Server; using MoShiro.Protocol; namespace MoShiro.Backend.Application; /// MoShiro 在 Fleet 之上的接入点:注册之后补全公开地址与端口。 public sealed class MoShiroEnrollmentHook : IEnrollmentHook { private readonly IAgentStore _agents; private readonly AgentService _service; public MoShiroEnrollmentHook(IAgentStore agents, AgentService service) { _agents = agents; _service = service; } public async ValueTask OnEnrolledAsync(NodeRecord node, EnrollmentContext context, CancellationToken ct) { if (node.Id.Value.StartsWith(ProbeId.Prefix, StringComparison.Ordinal)) { // Probe:新的对等面公钥要进每个 Agent 的名单,Probe 自己也要拿到 Agent 列表 await _service.AfterAgentSetChangedAsync(ct); return; } AgentEnrollMeta meta = new(); if (context.ProductMeta is { } raw) { try { meta = raw.Deserialize(MoShiroJsonContext.Default.AgentEnrollMeta) ?? meta; } catch (JsonException) { // 元数据格式不对就用默认值,节点之后可以通过 hello 补全 } } // 注册请求带了 public_host 就用它;否则来源地址是公网地址时用来源地址(私网或隧道出口不用,等 hello 里的本机地址) string host = !string.IsNullOrWhiteSpace(meta.PublicHost) ? meta.PublicHost : System.Net.IPAddress.TryParse(context.RemoteIp, out System.Net.IPAddress? remote) && PublicAddress.IsPublicIpv4(remote) ? remote.ToString() : ""; await _agents.ApplyEnrollmentAsync(node.Id, new AgentEnrollment(host, meta.ProbeTcpPort, meta.ProbeUdpPorts, meta.Kinds), ct); if (await _agents.GetAsync(node.Id, ct) is { } enrolled) { (_, string? observedV6) = AgentAddressPolicy.Choose(enrolled, [], context.RemoteIp); await _agents.ApplyAutoAddressesAsync(node.Id, null, observedV6, ct); } if (node.Status == NodeStatus.Active) { await _service.AfterAgentSetChangedAsync(ct); } } } /// 节点上线、下线、hello、心跳:更新运行时状态,补全 Agent 信息,并推送给 SSE。 public sealed class MoShiroNodeEventSink : INodeEventSink { private readonly IAgentStore _agents; private readonly AgentRuntime _runtime; private readonly IEventStore _events; private readonly LiveHub _live; private readonly AgentService _service; private readonly NodeInfoService _nodes; private readonly ReleaseService _releases; private readonly ScheduleService _schedule; private readonly IProbeStore _probes; public MoShiroNodeEventSink(IAgentStore agents, AgentRuntime runtime, IEventStore events, LiveHub live, AgentService service, NodeInfoService nodes, ReleaseService releases, ScheduleService schedule, IProbeStore probes) { _releases = releases; _schedule = schedule; _probes = probes; _nodes = nodes; _agents = agents; _runtime = runtime; _events = events; _live = live; _service = service; } public async ValueTask OnEventAsync(NodeEvent nodeEvent, CancellationToken ct) { NodeId id = nodeEvent.Node.Id; if (id.Value.StartsWith(ProbeId.Prefix, StringComparison.Ordinal)) { await OnProbeEventAsync(id, nodeEvent, ct); return; } switch (nodeEvent.Kind) { case NodeEventKind.Online: _live.Publish(new NodeSignal("agent", id.Value, true, nodeEvent.AtMs)); await _events.AddAsync(id, "agent_online", nodeEvent.AtMs, null, ct); break; case NodeEventKind.Offline: _runtime.MarkOffline(id); _releases.RecordStatus(id, null); _live.Publish(new NodeSignal("agent", id.Value, false, nodeEvent.AtMs)); await _events.AddAsync(id, "agent_offline", nodeEvent.AtMs, null, ct); break; case NodeEventKind.Hello when nodeEvent.Hello is { } hello: await ApplyHelloAsync(id, hello, ct); break; case NodeEventKind.Heartbeat when nodeEvent.Extra is { } extra: try { AgentRuntimeState state = _runtime.Get(id); AgentHeartbeatExtra? beat = extra.Deserialize(MoShiroJsonContext.Default.AgentHeartbeatExtra); state.LastHeartbeat = beat; state.LastHeartbeatAtMs = nodeEvent.AtMs; _releases.RecordStatus(id, beat?.Update); if (beat is not null) { state.ReplaceProbes(beat.Probes.Select(p => new ConnectedProbeInfo(p.Id.Value, p.Version, p.Remote, p.SinceUtcMs, p.IpVersion))); foreach (ConnectedProbe p in beat.Probes) { _nodes.ObserveProbeInBackground(p.Id.Value, p.Remote); } } } catch (JsonException) { // 忽略格式不正确的附加字段 } break; } } /// Probe 自己连上来了:hello 里带着它的系统、架构和构建信息;上线下线只推给 SSE,不记事件。 private async ValueTask OnProbeEventAsync(NodeId id, NodeEvent nodeEvent, CancellationToken ct) { switch (nodeEvent.Kind) { case NodeEventKind.Online: _live.Publish(new NodeSignal("probe", id.Value, true, nodeEvent.AtMs)); break; case NodeEventKind.Hello: if (nodeEvent.Hello?.Meta is { } raw) { try { ProbeHelloMeta? meta = raw.Deserialize(MoShiroJsonContext.Default.ProbeHelloMeta); await _nodes.SetPlatformAsync(id.Value, NodeKinds.Probe, meta?.Platform, ct); if (meta?.Ipv6 is { } ipv6) { await _probes.SetIpv6DiagnosticsAsync(id.Value, JsonSerializer.Serialize(ipv6, MoShiroJsonContext.Default.Ipv6Diagnostic), ct); } } catch (JsonException) { // 格式不对就忽略 } } await _releases.OfferToNodeAsync(id, ct); await _schedule.SendToNodeAsync(id, ct); break; case NodeEventKind.Offline: _live.Publish(new NodeSignal("probe", id.Value, false, nodeEvent.AtMs)); _releases.RecordStatus(id, null); break; case NodeEventKind.Heartbeat when nodeEvent.Extra is { } extra: try { ProbeHeartbeatExtra? heartbeat = extra.Deserialize(MoShiroJsonContext.Default.ProbeHeartbeatExtra); _releases.RecordStatus(id, heartbeat?.Update); if (heartbeat?.Ipv6 is { } ipv6) { await _probes.SetIpv6DiagnosticsAsync(id.Value, JsonSerializer.Serialize(ipv6, MoShiroJsonContext.Default.Ipv6Diagnostic), ct); } } catch (JsonException) { // 忽略格式不正确的附加字段 } break; } } private async ValueTask ApplyHelloAsync(NodeId id, HelloParams hello, CancellationToken ct) { var kinds = new List(); foreach (string capability in hello.Capabilities) { if (capability.StartsWith(MoShiroCapabilities.KindsPrefix, StringComparison.Ordinal)) { kinds.AddRange(capability[MoShiroCapabilities.KindsPrefix.Length..].Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)); } } AgentHelloMeta meta = new(); if (hello.Meta is { } raw) { try { meta = raw.Deserialize(MoShiroJsonContext.Default.AgentHelloMeta) ?? meta; } catch (JsonException) { // 忽略 } } // Agent 上报 TLS 证书的公钥:指纹由面板自己算,不信节点报的指纹 byte[]? tlsKey = null; byte[]? spki = null; if (meta.TlsPublicKey is not null) { try { byte[] candidate = Convert.FromBase64String(meta.TlsPublicKey); if (Fleet.Identity.NodeIdentity.IsValidPublicKey(candidate)) { tlsKey = candidate; spki = Fleet.Identity.NodeIdentity.Fingerprint(candidate); } } catch (FormatException) { // 忽略 } } AgentRecord? before = await _agents.GetAsync(id, ct); string? limitsJson = meta.LocalLimits is null ? null : JsonSerializer.Serialize(meta.LocalLimits, LocalLimitsJson.Default.LocalLimits); string? ipv6Json = meta.Ipv6 is null ? null : JsonSerializer.Serialize(meta.Ipv6, MoShiroJsonContext.Default.Ipv6Diagnostic); await _agents.ApplyHelloAsync(id, new AgentHello(kinds, hello.NodeVersion, limitsJson, spki is { Length: 32 } ? spki : null, meta.ProbeTcpPort, meta.ProbeUdpPorts, meta.Ipv6Supported, ipv6Json, meta.Addresses, tlsKey), ct); // 自动识别对外地址:Agent 上报的公网地址;手填的不动 if (meta.Addresses is not null && await _agents.GetAsync(id, ct) is { } reported) { (string? publicHost, string? ipv6Host) = AgentAddressPolicy.Choose(reported, meta.Addresses, null); await _agents.ApplyAutoAddressesAsync(id, publicHost, ipv6Host, ct); } // 端口、证书指纹或对外地址变了,Probe 需要新的列表 AgentRecord? after = await _agents.GetAsync(id, ct); _nodes.ObserveAgentInBackground(id.Value, after?.PublicHost, meta.Platform); await _releases.OfferToNodeAsync(id, ct); await _schedule.SendToNodeAsync(id, ct); if (before is not null && after is not null && (!before.TlsSpki.AsSpan().SequenceEqual(after.TlsSpki) || !(before.TlsPublicKey ?? []).AsSpan().SequenceEqual(after.TlsPublicKey ?? []) || before.ProbeTcpPort != after.ProbeTcpPort || !before.ProbeUdpPorts.SequenceEqual(after.ProbeUdpPorts) || before.Ipv6Supported != after.Ipv6Supported || before.PublicHost != after.PublicHost || before.Ipv6Host != after.Ipv6Host) && after.Status == NodeStatus.Active) { await _service.AfterAgentSetChangedAsync(ct); } } } [System.Text.Json.Serialization.JsonSerializable(typeof(LocalLimits))] internal sealed partial class LocalLimitsJson : System.Text.Json.Serialization.JsonSerializerContext;