feat!: pre-handshake knock on the agent TCP ports; probes get the agent certificate public key [CI]
ci/woodpecker/push/main-amd64-build Pipeline was successful

This commit is contained in:
justme committed 2026-10-07 18:56:40 +08:00
1 parent f03988d68b
commit b99b95c738
31 files changed
+539 -61

No files matched your search

@@ -157,7 +157,8 @@ public sealed partial class AgentService
IReadOnlyList<AgentRecord> agents = await _agents.ListAsync(ct);
long version = long.TryParse(await _settings.GetAsync(TaskPlanner.ProbeAgentsVersionKey, ct), out long v) ? v : 0;
var result = new ProbeAgentList { Version = version };
foreach (AgentRecord agent in agents.Where(a => a.Status == NodeStatus.Active && a.PublicHost.Length > 0 && links.IsLinked(probeId, a.Id.Value)))
// 没有上报证书公钥的 Agent(旧版本)还不能被 Probe 连接:Probe 需要它来固定证书和派生敲门密钥
foreach (AgentRecord agent in agents.Where(a => a.Status == NodeStatus.Active && a.PublicHost.Length > 0 && a.TlsPublicKey is { Length: > 0 } && links.IsLinked(probeId, a.Id.Value)))
{
result.Agents.Add(new AgentEndpoint
{
@@ -167,7 +168,7 @@ public sealed partial class AgentService
Ipv6Host = agent.Ipv6Supported && agent.Ipv6Host.Length > 0 ? agent.Ipv6Host : null,
TcpPort = agent.ProbeTcpPort,
UdpPorts = [.. agent.ProbeUdpPorts],
Spki = Convert.ToBase64String(agent.TlsSpki),
PublicKey = Convert.ToBase64String(agent.TlsPublicKey!),
});
}
@@ -209,12 +209,19 @@ public sealed class MoShiroNodeEventSink : INodeEventSink
}
}
// Agent 上报 TLS 证书的公钥:指纹由面板自己算,不信节点报的指纹
byte[]? tlsKey = null;
byte[]? spki = null;
if (meta.TlsSpki is not null)
if (meta.TlsPublicKey is not null)
{
try
{
spki = Convert.FromBase64String(meta.TlsSpki);
byte[] candidate = Convert.FromBase64String(meta.TlsPublicKey);
if (Fleet.Identity.NodeIdentity.IsValidPublicKey(candidate))
{
tlsKey = candidate;
spki = Fleet.Identity.NodeIdentity.Fingerprint(candidate);
}
}
catch (FormatException)
{
@@ -225,7 +232,7 @@ public sealed class MoShiroNodeEventSink : INodeEventSink
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), ct);
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)
@@ -240,7 +247,7 @@ public sealed class MoShiroNodeEventSink : INodeEventSink
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.ProbeTcpPort != after.ProbeTcpPort || !before.ProbeUdpPorts.SequenceEqual(after.ProbeUdpPorts) || before.Ipv6Supported != after.Ipv6Supported || before.PublicHost != after.PublicHost || before.Ipv6Host != after.Ipv6Host)
&& (!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);
@@ -28,7 +28,8 @@ public sealed record AgentRecord(
string? Ipv6DiagnosticsJson = null,
bool PublicHostManual = false,
bool Ipv6HostManual = false,
IReadOnlyList<string>? ReportedAddresses = null);
IReadOnlyList<string>? ReportedAddresses = null,
byte[]? TlsPublicKey = null);
/// <summary>管理员可以修改的字段(merge-patch 语义:为 null 的字段不改)。</summary>
public sealed class AgentPatch
@@ -59,7 +60,8 @@ public sealed record AgentHello(
IReadOnlyList<int>? ProbeUdpPorts,
bool Ipv6Supported = false,
string? Ipv6DiagnosticsJson = null,
IReadOnlyList<string>? Addresses = null);
IReadOnlyList<string>? Addresses = null,
byte[]? TlsPublicKey = null);
/// <summary>注册之后 MoShiro 的补充:公开地址和端口。</summary>
public sealed record AgentEnrollment(string PublicHost, int ProbeTcpPort, IReadOnlyList<int> ProbeUdpPorts, IReadOnlyList<string> Kinds);