agent: unify listening and dialing with directed mutual measurements

Replace Probe with Agent across node runtime, Fleet contracts, backend state, API and storage. Report incoming sessions and outgoing link health through each Agent uplink, and retain directed link controls and retry.

Update protocol fixtures and end-to-end coverage for the unified model; accept requested cancellation during the Fleet timeout test cleanup.
This commit is contained in:
justme committed 2026-10-21 11:55:36 +08:00
1 parent d5ebf29e57
commit ad5efc62ff
206 files changed
+4202 -4964

No files matched your search

@@ -1,5 +1,6 @@
using Fleet.Core;
using Fleet.Server;
using MoShiro.Protocol;
namespace MoShiro.Backend.Application;
@@ -10,8 +11,8 @@ public sealed record AgentRecord(
byte[] PublicKey,
byte[] TlsSpki,
string PublicHost,
int ProbeTcpPort,
IReadOnlyList<int> ProbeUdpPorts,
int TcpPort,
IReadOnlyList<int> QuicPorts,
IReadOnlyList<string> Labels,
bool PublicVisible,
IReadOnlyList<string> Kinds,
@@ -30,7 +31,12 @@ public sealed record AgentRecord(
bool Ipv6HostManual = false,
IReadOnlyList<string>? ReportedAddresses = null,
byte[]? TlsPublicKey = null,
string? AddedBy = null);
string? AddedBy = null,
bool Listens = false)
{
/// <summary>可以作为拨号目标:激活、会监听、有公开地址和证书公钥(拨号方需要它固定证书、派生敲门密钥)。</summary>
public bool Dialable => Status == NodeStatus.Active && Listens && PublicHost.Length > 0 && TlsPublicKey is { Length: > 0 };
}
/// <summary>管理员可以修改的字段(merge-patch 语义:为 null 的字段不改)。</summary>
public sealed class AgentPatch
@@ -51,21 +57,16 @@ public sealed class AgentPatch
public bool? PublicVisible { get; init; }
}
/// <summary>来自 <c>fleet.hello</c> 的信息。</summary>
/// <summary>来自 <c>fleet.hello</c> 的信息。不监听的节点没有 <see cref="Listen"/>。</summary>
public sealed record AgentHello(
IReadOnlyList<string> Kinds,
string? AgentVersion,
string? LocalLimitsJson,
byte[]? TlsSpki,
int? ProbeTcpPort,
IReadOnlyList<int>? ProbeUdpPorts,
bool Ipv6Supported = false,
string? Ipv6DiagnosticsJson = null,
IReadOnlyList<string>? Addresses = null,
byte[]? TlsPublicKey = null);
ListenInfo? Listen,
string? Ipv6DiagnosticsJson = null);
/// <summary>注册之后 MoShiro 的补充:公开地址和端口。</summary>
public sealed record AgentEnrollment(string PublicHost, int ProbeTcpPort, IReadOnlyList<int> ProbeUdpPorts, IReadOnlyList<string> Kinds);
/// <summary>注册之后 MoShiro 的补充:公开地址和端口(不监听的节点没有端口)。</summary>
public sealed record AgentEnrollment(string PublicHost, int TcpPort, IReadOnlyList<int> QuicPorts, IReadOnlyList<string> Kinds);
public sealed record JoinTokenInfo(
string Id,
@@ -79,9 +80,3 @@ public sealed record JoinTokenInfo(
long CreatedAtMs,
string Kind = "",
string? CreatedBy = null);
/// <summary>
/// <see cref="PublicKey"/> 是连 Backend 的身份公钥,<see cref="PeerPublicKey"/> 是对等面(连 Agent)的公钥,两把是节点自己生成的独立密钥;
/// Agent 的 <c>peers</c> 里放的是后者。
/// </summary>
public sealed record ProbeRecord(string Id, string Name, byte[] PublicKey, long CreatedAtMs, long? RevokedAtMs, string? Ipv6DiagnosticsJson = null, byte[]? PeerPublicKey = null, string? AddedBy = null);
@@ -20,7 +20,7 @@ public sealed record TaskRecord(
public sealed record TestRecord(
TestId Id,
NodeId AgentId,
ProbeId ProbeId,
NodeId DialerId,
string Plan,
string ParamsJson,
string Trigger,