Join codes need no passkey touch and carry only public values (genesis fingerprint, owner credentials, installer hashes). Nodes pin the genesis and owners at enrollment and only follow passkey-signed operator set versions. The panel's server key is listed in the set; pinned nodes require its signature and run the control link under AES-256-GCM. Password sessions carry no capabilities. The updater adopts a next_key from a signed manifest. Bump version to 0.2.0. [CI SKIP]
116 lines
3.9 KiB
C#
116 lines
3.9 KiB
C#
using System.Text.Json;
|
|
using Fleet.Core;
|
|
using Fleet.Hosting;
|
|
using MoShiro.Protocol;
|
|
|
|
namespace MoShiro.Probe;
|
|
|
|
/// <summary>
|
|
/// Probe 要连接的 Agent 列表:由面板作为期望状态(<c>probe_agents</c>)直接下发;面板连不上时用上一次保存的 <c>agents.json</c>。
|
|
/// 列表变化时用 <see cref="ConfigStore"/> 写回 <c>agents.json</c>(这是配置,不是测量数据;只在变化时写,保护路由器的闪存)。
|
|
/// </summary>
|
|
public sealed class AgentDirectory
|
|
{
|
|
public const string FileName = "agents.json";
|
|
|
|
private readonly object _gate = new();
|
|
private readonly ConfigStore _store;
|
|
private readonly INodeLog _log;
|
|
private readonly TrustAnchor _trust;
|
|
private List<KnownAgent> _agents = [];
|
|
private long _version;
|
|
|
|
public AgentDirectory(ConfigStore store, string configDirectory, INodeLog log, TrustAnchor? trust = null)
|
|
{
|
|
_trust = trust ?? TrustAnchor.None;
|
|
_store = store;
|
|
_log = log;
|
|
string cache = Path.Combine(configDirectory, FileName);
|
|
if (File.Exists(cache))
|
|
{
|
|
try
|
|
{
|
|
AgentsCache saved = NodeConfig.Load(cache, ProbeConfigContext.Default.AgentsCache);
|
|
_agents = saved.Agents;
|
|
_version = saved.Version;
|
|
}
|
|
catch (Exception ex) when (ex is IOException or InvalidDataException or JsonException or InvalidOperationException)
|
|
{
|
|
_log.Warn("agents.json is unreadable, waiting for the panel", ("err", ex.Message));
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>列表变化(调和连接)。</summary>
|
|
public event Action? Changed;
|
|
|
|
public long Version
|
|
{
|
|
get
|
|
{
|
|
lock (_gate)
|
|
{
|
|
return _version;
|
|
}
|
|
}
|
|
}
|
|
|
|
public IReadOnlyList<KnownAgent> Agents
|
|
{
|
|
get
|
|
{
|
|
lock (_gate)
|
|
{
|
|
return [.. _agents];
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>应用面板下发的列表。版本不比当前新的忽略(乱序到达的旧推送)。</summary>
|
|
public void Apply(ProbeAgentList result)
|
|
{
|
|
var next = new List<KnownAgent>();
|
|
_trust.Update(result.TrustChain);
|
|
foreach (AgentEndpoint agent in result.Agents)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(agent.Host))
|
|
{
|
|
next.Add(new KnownAgent { AgentId = agent.AgentId.Value, Host = agent.Host, TcpPort = agent.TcpPort, PublicKey = agent.PublicKey });
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(agent.Ipv6Host))
|
|
{
|
|
next.Add(new KnownAgent { AgentId = agent.AgentId.Value, Host = agent.Ipv6Host, TcpPort = agent.TcpPort, PublicKey = agent.PublicKey, IpVersion = IpVersions.V6 });
|
|
}
|
|
}
|
|
lock (_gate)
|
|
{
|
|
if (result.Version < _version || (result.Version == _version && Same(next, _agents)))
|
|
{
|
|
return;
|
|
}
|
|
|
|
_version = result.Version;
|
|
_agents = next;
|
|
}
|
|
|
|
try
|
|
{
|
|
_store.Write(FileName, JsonSerializer.SerializeToUtf8Bytes(new AgentsCache { Version = result.Version, Agents = next }, ProbeConfigContext.Default.AgentsCache));
|
|
}
|
|
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
|
{
|
|
_log.Warn("could not save agents.json", ("err", ex.Message));
|
|
}
|
|
|
|
_log.Info("agent list updated", ("version", result.Version), ("agents", next.Count));
|
|
Changed?.Invoke();
|
|
}
|
|
|
|
private static bool Same(List<KnownAgent> a, List<KnownAgent> b) =>
|
|
a.Count == b.Count && a.Zip(b).All(p => Equal(p.First, p.Second));
|
|
|
|
public static bool Equal(KnownAgent a, KnownAgent b) =>
|
|
a.AgentId == b.AgentId && a.IpVersion == b.IpVersion && a.Host == b.Host && a.TcpPort == b.TcpPort && a.PublicKey == b.PublicKey;
|
|
}
|