feat!: probes generate their own keys and enroll with a join token; node and peer keys are separate [CI]
ci/woodpecker/push/main-amd64-build Pipeline was successful

This commit is contained in:
justme committed 2026-10-07 16:57:34 +08:00
1 parent 1ff847e45a
commit a9f562441f
51 files changed
+505 -749

No files matched your search

@@ -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 = 7;
public const int CurrentVersion = 8;
public static void Migrate(SqliteConnection connection)
{
@@ -110,6 +110,16 @@ internal static class Schema
connection.Exec("PRAGMA user_version = 7");
transaction.Commit();
}
if (version < 8)
{
// Probe 用加入令牌注册:令牌带种类;Probe 有两把独立的密钥,对等面公钥单独存
using SqliteTransaction transaction = connection.BeginTransaction();
connection.Exec("ALTER TABLE join_tokens ADD COLUMN kind TEXT NOT NULL DEFAULT ''");
connection.Exec("ALTER TABLE probes ADD COLUMN peer_pubkey BLOB");
connection.Exec("PRAGMA user_version = 8");
transaction.Commit();
}
}
/// <summary>
@@ -56,4 +56,6 @@ internal static class Sql
public static string? NullableString(this SqliteDataReader reader, int ordinal) => reader.IsDBNull(ordinal) ? null : reader.GetString(ordinal);
public static byte[] Bytes(this SqliteDataReader reader, int ordinal) => (byte[])reader.GetValue(ordinal);
public static byte[]? NullableBytes(this SqliteDataReader reader, int ordinal) => reader.IsDBNull(ordinal) ? null : (byte[])reader.GetValue(ordinal);
}
@@ -138,8 +138,8 @@ public sealed class SqliteAgentStore : IAgentStore, IFleetStore
byte[] hash = tokenSha256.ToArray();
return _db.ReadAsync(
c => c.QueryOne(
"SELECT id, token_sha256, note, labels, auto_approve, max_uses, uses, expires_at, revoked_at, created_at FROM join_tokens WHERE token_sha256 = @h",
r => new JoinTokenRecord(r.GetString(0), r.Bytes(1), r.NullableString(2), StorageJson.Strings(r.GetString(3)), r.GetInt64(4) != 0, r.GetInt32(5), r.GetInt32(6), r.GetInt64(7), r.NullableLong(8), r.GetInt64(9)),
"SELECT id, token_sha256, note, labels, auto_approve, max_uses, uses, expires_at, revoked_at, created_at, kind FROM join_tokens WHERE token_sha256 = @h",
r => new JoinTokenRecord(r.GetString(0), r.Bytes(1), r.NullableString(2), StorageJson.Strings(r.GetString(3)), r.GetInt64(4) != 0, r.GetInt32(5), r.GetInt32(6), r.GetInt64(7), r.NullableLong(8), r.GetInt64(9), r.GetString(10)),
("@h", hash)),
ct);
}
@@ -154,6 +154,11 @@ public sealed class SqliteAgentStore : IAgentStore, IFleetStore
public ValueTask<Result<NodeRecord>> CreateNodeAsync(NewNode node, CancellationToken ct) =>
_db.WriteAsync(c =>
{
if (node.Kind == JoinTokenService.ProbeRole)
{
return CreateProbe(c, node);
}
if (node.Status != NodeStatus.Pending && NameTaken(c, node.Name, node.Id))
{
return Result<NodeRecord>.Fail(FleetErrors.NameConflict, "a node with this name already exists");
@@ -166,6 +171,25 @@ public sealed class SqliteAgentStore : IAgentStore, IFleetStore
return Result<NodeRecord>.Ok(ToNode(GetInternal(c, node.Id)!, node.MetaJson));
}, ct);
/// <summary>Probe 用加入令牌注册:直接进 probes 表,有令牌就算批准(Probe 没有 pending 状态);两把公钥都是它自己生成的。</summary>
private static Result<NodeRecord> CreateProbe(SqliteConnection c, NewNode node)
{
if (node.TlsPublicKey is not { Length: > 0 } peerKey)
{
return Result<NodeRecord>.Fail(FleetErrors.InvalidParams, "a probe needs its peer key");
}
if (c.Scalar<long>("SELECT COUNT(*) FROM probes WHERE name = @name COLLATE NOCASE", ("@name", node.Name)) > 0)
{
return Result<NodeRecord>.Fail(FleetErrors.NameConflict, "a probe with this name already exists");
}
c.Exec(
"INSERT INTO probes (id, name, pubkey, peer_pubkey, created_at) VALUES (@id, @name, @pubkey, @peer, @now)",
("@id", node.Id.Value), ("@name", node.Name), ("@pubkey", node.PublicKey), ("@peer", peerKey), ("@now", node.NowMs));
return Result<NodeRecord>.Ok(new NodeRecord(node.Id, node.Name, NodeStatus.Active, node.PublicKey, node.TlsSpki, node.Labels, node.MetaJson, node.NodeVersion, node.NowMs, null));
}
/// <summary>Agent 在 agents 表里;Probe 的身份在 probes 表里(吊销之后按「禁用」处理,连不上)。</summary>
async ValueTask<NodeRecord?> IFleetStore.GetNodeAsync(NodeId id, CancellationToken ct) =>
await GetAsync(id, ct) is { } agent
@@ -15,21 +15,21 @@ public sealed class SqliteJoinTokenStore : IJoinTokenStore
_db = db;
}
public ValueTask<JoinTokenInfo> CreateAsync(string id, byte[] tokenSha256, string? note, IReadOnlyList<string> labels, bool autoApprove, int maxUses, long expiresAtMs, long nowMs, CancellationToken ct) =>
public ValueTask<JoinTokenInfo> CreateAsync(string id, byte[] tokenSha256, string? note, IReadOnlyList<string> labels, bool autoApprove, string kind, int maxUses, long expiresAtMs, long nowMs, CancellationToken ct) =>
_db.WriteAsync(c =>
{
c.Exec(
"INSERT INTO join_tokens (id, token_sha256, note, labels, auto_approve, max_uses, expires_at, created_at) VALUES (@id, @h, @note, @labels, @auto, @max, @exp, @now)",
("@id", id), ("@h", tokenSha256), ("@note", note), ("@labels", StorageJson.ToJson(labels)), ("@auto", autoApprove ? 1 : 0),
"INSERT INTO join_tokens (id, token_sha256, note, labels, auto_approve, kind, max_uses, expires_at, created_at) VALUES (@id, @h, @note, @labels, @auto, @kind, @max, @exp, @now)",
("@id", id), ("@h", tokenSha256), ("@note", note), ("@labels", StorageJson.ToJson(labels)), ("@auto", autoApprove ? 1 : 0), ("@kind", kind),
("@max", maxUses), ("@exp", expiresAtMs), ("@now", nowMs));
return new JoinTokenInfo(id, note, labels, autoApprove, maxUses, 0, expiresAtMs, null, nowMs);
return new JoinTokenInfo(id, note, labels, autoApprove, maxUses, 0, expiresAtMs, null, nowMs, kind);
}, ct);
public ValueTask<IReadOnlyList<JoinTokenInfo>> ListAsync(CancellationToken ct) =>
_db.ReadAsync<IReadOnlyList<JoinTokenInfo>>(
c => c.Query(
"SELECT id, note, labels, auto_approve, max_uses, uses, expires_at, revoked_at, created_at FROM join_tokens ORDER BY created_at DESC, id",
r => new JoinTokenInfo(r.GetString(0), r.NullableString(1), StorageJson.Strings(r.GetString(2)), r.GetInt64(3) != 0, r.GetInt32(4), r.GetInt32(5), r.GetInt64(6), r.NullableLong(7), r.GetInt64(8))),
"SELECT id, note, labels, auto_approve, max_uses, uses, expires_at, revoked_at, created_at, kind FROM join_tokens ORDER BY created_at DESC, id",
r => new JoinTokenInfo(r.GetString(0), r.NullableString(1), StorageJson.Strings(r.GetString(2)), r.GetInt64(3) != 0, r.GetInt32(4), r.GetInt32(5), r.GetInt64(6), r.NullableLong(7), r.GetInt64(8), r.GetString(9))),
ct);
public ValueTask<bool> RevokeAsync(string id, long nowMs, CancellationToken ct) =>
@@ -50,12 +50,12 @@ public sealed class SqliteProbeStore : IProbeStore
public ValueTask<IReadOnlyList<ProbeRecord>> ListAsync(CancellationToken ct) =>
_db.ReadAsync<IReadOnlyList<ProbeRecord>>(
c => c.Query("SELECT id, name, pubkey, created_at, revoked_at, ipv6_diagnostics FROM probes ORDER BY created_at, id", r => new ProbeRecord(r.GetString(0), r.GetString(1), r.Bytes(2), r.GetInt64(3), r.NullableLong(4), r.NullableString(5))),
c => c.Query("SELECT id, name, pubkey, created_at, revoked_at, ipv6_diagnostics, peer_pubkey FROM probes ORDER BY created_at, id", r => new ProbeRecord(r.GetString(0), r.GetString(1), r.Bytes(2), r.GetInt64(3), r.NullableLong(4), r.NullableString(5), r.NullableBytes(6))),
ct);
public ValueTask<ProbeRecord?> GetAsync(string id, CancellationToken ct) =>
_db.ReadAsync(
c => c.QueryOne("SELECT id, name, pubkey, created_at, revoked_at, ipv6_diagnostics FROM probes WHERE id = @id", r => new ProbeRecord(r.GetString(0), r.GetString(1), r.Bytes(2), r.GetInt64(3), r.NullableLong(4), r.NullableString(5)), ("@id", id)),
c => c.QueryOne("SELECT id, name, pubkey, created_at, revoked_at, ipv6_diagnostics, peer_pubkey FROM probes WHERE id = @id", r => new ProbeRecord(r.GetString(0), r.GetString(1), r.Bytes(2), r.GetInt64(3), r.NullableLong(4), r.NullableString(5), r.NullableBytes(6)), ("@id", id)),
ct);
public ValueTask SetIpv6DiagnosticsAsync(string id, string diagnosticsJson, CancellationToken ct) =>
@@ -71,9 +71,9 @@ public sealed class SqliteProbeStore : IProbeStore
_db.ExecuteAsync(c =>
{
c.Exec(
"INSERT INTO probes (id, name, pubkey, created_at, revoked_at) VALUES (@id, @name, @pubkey, @at, @revoked) " +
"ON CONFLICT(id) DO UPDATE SET name = excluded.name, pubkey = excluded.pubkey, created_at = excluded.created_at, revoked_at = excluded.revoked_at",
("@id", probe.Id), ("@name", probe.Name), ("@pubkey", probe.PublicKey), ("@at", probe.CreatedAtMs), ("@revoked", probe.RevokedAtMs));
"INSERT INTO probes (id, name, pubkey, peer_pubkey, created_at, revoked_at) VALUES (@id, @name, @pubkey, @peer, @at, @revoked) " +
"ON CONFLICT(id) DO UPDATE SET name = excluded.name, pubkey = excluded.pubkey, peer_pubkey = excluded.peer_pubkey, created_at = excluded.created_at, revoked_at = excluded.revoked_at",
("@id", probe.Id), ("@name", probe.Name), ("@pubkey", probe.PublicKey), ("@peer", probe.PeerPublicKey), ("@at", probe.CreatedAtMs), ("@revoked", probe.RevokedAtMs));
}, ct);
}