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
ci/woodpecker/push/main-amd64-build Pipeline was successful
This commit is contained in:
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);
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace MoShiro.Backend.Storage.Sqlite;
|
||||
/// <summary>表结构。版本记录在 <c>PRAGMA user_version</c>;以后的迁移是按版本号追加的内置 SQL。</summary>
|
||||
internal static class Schema
|
||||
{
|
||||
public const int CurrentVersion = 9;
|
||||
public const int CurrentVersion = 10;
|
||||
|
||||
public static void Migrate(SqliteConnection connection)
|
||||
{
|
||||
@@ -131,6 +131,15 @@ internal static class Schema
|
||||
connection.Exec("PRAGMA user_version = 9");
|
||||
transaction.Commit();
|
||||
}
|
||||
|
||||
if (version < 10)
|
||||
{
|
||||
// 预握手敲门:Probe 需要 Agent 的证书公钥(不只是指纹)来派生配对密钥,由 Agent 在 hello 里上报
|
||||
using SqliteTransaction transaction = connection.BeginTransaction();
|
||||
connection.Exec("ALTER TABLE agents ADD COLUMN tls_pubkey BLOB");
|
||||
connection.Exec("PRAGMA user_version = 10");
|
||||
transaction.Commit();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -14,7 +14,7 @@ public sealed class SqliteAgentStore : IAgentStore, IFleetStore
|
||||
{
|
||||
private const string AgentColumns =
|
||||
"id, name, status, pubkey, tls_spki, public_host, probe_tcp_port, probe_udp_ports, labels, public_visible, kinds, agent_version, local_limits, " +
|
||||
"task_version, applied_task_version, rejected, created_at, last_seen_at, row_version, ipv6_host, ipv6_supported, ipv6_diagnostics, public_host_manual, ipv6_host_manual, reported_addresses";
|
||||
"task_version, applied_task_version, rejected, created_at, last_seen_at, row_version, ipv6_host, ipv6_supported, ipv6_diagnostics, public_host_manual, ipv6_host_manual, reported_addresses, tls_pubkey";
|
||||
|
||||
private readonly SqliteDatabase _db;
|
||||
|
||||
@@ -123,11 +123,11 @@ public sealed class SqliteAgentStore : IAgentStore, IFleetStore
|
||||
_db.ExecuteAsync(c =>
|
||||
{
|
||||
c.Exec(
|
||||
"UPDATE agents SET kinds = @kinds, agent_version = @version, local_limits = COALESCE(@limits, local_limits), tls_spki = COALESCE(@spki, tls_spki), " +
|
||||
"UPDATE agents SET kinds = @kinds, agent_version = @version, local_limits = COALESCE(@limits, local_limits), tls_spki = COALESCE(@spki, tls_spki), tls_pubkey = COALESCE(@pub, tls_pubkey), " +
|
||||
"probe_tcp_port = COALESCE(@tcp, probe_tcp_port), probe_udp_ports = COALESCE(@udp, probe_udp_ports), ipv6_supported = @v6, ipv6_diagnostics = @diag, " +
|
||||
"reported_addresses = COALESCE(@addr, reported_addresses), row_version = row_version + 1 WHERE id = @id",
|
||||
("@id", id.Value), ("@kinds", StorageJson.ToJson(hello.Kinds)), ("@version", hello.AgentVersion), ("@limits", hello.LocalLimitsJson),
|
||||
("@spki", hello.TlsSpki), ("@tcp", hello.ProbeTcpPort), ("@udp", hello.ProbeUdpPorts is null ? null : StorageJson.ToJson(hello.ProbeUdpPorts)), ("@v6", hello.Ipv6Supported ? 1 : 0), ("@diag", hello.Ipv6DiagnosticsJson),
|
||||
("@spki", hello.TlsSpki), ("@pub", hello.TlsPublicKey), ("@tcp", hello.ProbeTcpPort), ("@udp", hello.ProbeUdpPorts is null ? null : StorageJson.ToJson(hello.ProbeUdpPorts)), ("@v6", hello.Ipv6Supported ? 1 : 0), ("@diag", hello.Ipv6DiagnosticsJson),
|
||||
("@addr", hello.Addresses is null ? null : StorageJson.ToJson(hello.Addresses)));
|
||||
}, ct);
|
||||
|
||||
@@ -266,7 +266,8 @@ public sealed class SqliteAgentStore : IAgentStore, IFleetStore
|
||||
r.NullableString(21),
|
||||
r.GetInt64(22) != 0,
|
||||
r.GetInt64(23) != 0,
|
||||
StorageJson.Strings(r.GetString(24)));
|
||||
StorageJson.Strings(r.GetString(24)),
|
||||
r.NullableBytes(25));
|
||||
|
||||
private static NodeRecord ToNode(AgentRecord a, string? metaJson) =>
|
||||
new(a.Id, a.Name, a.Status, a.PublicKey, a.TlsSpki, a.Labels, metaJson, a.AgentVersion, a.CreatedAtMs, a.LastSeenAtMs);
|
||||
|
||||
Reference in new issue
Block a user