feat: detect an agent's public IPv4 and IPv6 addresses automatically, manual values still win [CI]
ci/woodpecker/push/main-amd64-build Pipeline was successful

This commit is contained in:
justme committed 2026-10-07 18:12:17 +08:00
1 parent 9966ea5d63
commit f03988d68b
26 files changed
+517 -33

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 = 8;
public const int CurrentVersion = 9;
public static void Migrate(SqliteConnection connection)
{
@@ -120,6 +120,17 @@ internal static class Schema
connection.Exec("PRAGMA user_version = 8");
transaction.Commit();
}
if (version < 9)
{
// 对外地址自动识别:手填的标记为 manual,自动识别不会覆盖;Agent 上报的地址存下来,撤销手填时马上能重新识别
using SqliteTransaction transaction = connection.BeginTransaction();
connection.Exec("ALTER TABLE agents ADD COLUMN public_host_manual INTEGER NOT NULL DEFAULT 0");
connection.Exec("ALTER TABLE agents ADD COLUMN ipv6_host_manual INTEGER NOT NULL DEFAULT 0");
connection.Exec("ALTER TABLE agents ADD COLUMN reported_addresses TEXT NOT NULL DEFAULT '[]'");
connection.Exec("PRAGMA user_version = 9");
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";
"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";
private readonly SqliteDatabase _db;
@@ -60,11 +60,18 @@ public sealed class SqliteAgentStore : IAgentStore, IFleetStore
return Result<AgentRecord>.Fail(MoShiroErrors.NameConflict, "another agent already uses this name");
}
// 手填就标记为 manual(自动识别不再覆盖);撤销手填回到自动,值留到下一次识别
bool hostAuto = patch.PublicHostAuto == true;
bool v6Auto = patch.Ipv6HostAuto == true;
c.Exec(
"UPDATE agents SET name = @name, labels = @labels, public_host = @host, ipv6_host = @v6, public_visible = @visible, row_version = row_version + 1 WHERE id = @id",
"UPDATE agents SET name = @name, labels = @labels, public_host = @host, ipv6_host = @v6, public_host_manual = @hostManual, ipv6_host_manual = @v6Manual, " +
"public_visible = @visible, row_version = row_version + 1 WHERE id = @id",
("@id", id.Value), ("@name", patch.Name ?? current.Name),
("@labels", StorageJson.ToJson(patch.Labels ?? current.Labels)),
("@host", patch.PublicHost ?? current.PublicHost), ("@v6", patch.Ipv6Host ?? current.Ipv6Host), ("@visible", (patch.PublicVisible ?? current.PublicVisible) ? 1 : 0));
("@host", hostAuto ? current.PublicHost : patch.PublicHost ?? current.PublicHost), ("@v6", v6Auto ? current.Ipv6Host : patch.Ipv6Host ?? current.Ipv6Host),
("@hostManual", hostAuto ? 0 : patch.PublicHost is not null || current.PublicHostManual ? 1 : 0),
("@v6Manual", v6Auto ? 0 : patch.Ipv6Host is not null || current.Ipv6HostManual ? 1 : 0),
("@visible", (patch.PublicVisible ?? current.PublicVisible) ? 1 : 0));
return Result<AgentRecord>.Ok(GetInternal(c, id)!);
}, ct);
@@ -117,9 +124,21 @@ public sealed class SqliteAgentStore : IAgentStore, IFleetStore
{
c.Exec(
"UPDATE agents SET kinds = @kinds, agent_version = @version, local_limits = COALESCE(@limits, local_limits), tls_spki = COALESCE(@spki, tls_spki), " +
"probe_tcp_port = COALESCE(@tcp, probe_tcp_port), probe_udp_ports = COALESCE(@udp, probe_udp_ports), ipv6_supported = @v6, ipv6_diagnostics = @diag, row_version = row_version + 1 WHERE id = @id",
"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), ("@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);
public ValueTask ApplyAutoAddressesAsync(NodeId id, string? publicHost, string? ipv6Host, CancellationToken ct) =>
_db.ExecuteAsync(c =>
{
c.Exec(
"UPDATE agents SET public_host = CASE WHEN public_host_manual = 0 AND @host IS NOT NULL THEN @host ELSE public_host END, " +
"ipv6_host = CASE WHEN ipv6_host_manual = 0 AND @v6 IS NOT NULL THEN @v6 ELSE ipv6_host END, row_version = row_version + 1 " +
"WHERE id = @id AND ((public_host_manual = 0 AND @host IS NOT NULL AND public_host <> @host) OR (ipv6_host_manual = 0 AND @v6 IS NOT NULL AND ipv6_host <> @v6))",
("@id", id.Value), ("@host", publicHost), ("@v6", ipv6Host));
}, ct);
public ValueTask SetTaskVersionAsync(NodeId id, long version, string taskHash, CancellationToken ct) =>
@@ -244,7 +263,10 @@ public sealed class SqliteAgentStore : IAgentStore, IFleetStore
r.GetInt64(18),
r.GetString(19),
r.GetInt64(20) != 0,
r.NullableString(21));
r.NullableString(21),
r.GetInt64(22) != 0,
r.GetInt64(23) != 0,
StorageJson.Strings(r.GetString(24)));
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);