Replace Probe with Agent across node runtime, Fleet contracts, backend state, API and storage. Report incoming sessions and outgoing link health through each Agent uplink, and retain directed link controls and retry. Update protocol fixtures and end-to-end coverage for the unified model; accept requested cancellation during the Fleet timeout test cleanup.
117 lines
4.1 KiB
C#
117 lines
4.1 KiB
C#
using System.Text.Json;
|
|
using Fleet.Core;
|
|
using Fleet.Hosting;
|
|
using MoShiro.Protocol;
|
|
|
|
namespace MoShiro.Agent;
|
|
|
|
/// <summary>
|
|
/// 本节点要拨的监听节点列表:由面板作为期望状态(<c>dial_targets</c>)直接下发;面板连不上时用上一次保存的 <c>dial_targets.json</c>。
|
|
/// 列表变化时用 <see cref="ConfigStore"/> 写回 <c>agents.json</c>(这是配置,不是测量数据;只在变化时写,保护路由器的闪存)。
|
|
/// </summary>
|
|
public sealed class DialDirectory
|
|
{
|
|
public const string FileName = "dial_targets.json";
|
|
|
|
private readonly object _gate = new();
|
|
private readonly ConfigStore _store;
|
|
private readonly INodeLog _log;
|
|
private readonly TrustAnchor _trust;
|
|
private List<DialEndpoint> _targets = [];
|
|
private long _version;
|
|
|
|
public DialDirectory(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
|
|
{
|
|
DialCache saved = NodeConfig.Load(cache, AgentConfigContext.Default.DialCache);
|
|
_targets = saved.Targets;
|
|
_version = saved.Version;
|
|
}
|
|
catch (Exception ex) when (ex is IOException or InvalidDataException or JsonException or InvalidOperationException)
|
|
{
|
|
_log.Warn("dial_targets.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<DialEndpoint> Targets
|
|
{
|
|
get
|
|
{
|
|
lock (_gate)
|
|
{
|
|
return [.. _targets];
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>应用面板下发的列表。版本不比当前新的忽略(乱序到达的旧推送)。</summary>
|
|
public void Apply(DialTargets result)
|
|
{
|
|
var next = new List<DialEndpoint>();
|
|
_trust.Update(result.TrustChain);
|
|
foreach (DialTarget target in result.Targets)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(target.Host))
|
|
{
|
|
next.Add(new DialEndpoint { NodeId = target.NodeId.Value, Host = target.Host, TcpPort = target.TcpPort, QuicPorts = [.. target.QuicPorts], PublicKey = target.PublicKey });
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(target.Ipv6Host))
|
|
{
|
|
next.Add(new DialEndpoint { NodeId = target.NodeId.Value, Host = target.Ipv6Host, TcpPort = target.TcpPort, QuicPorts = [.. target.QuicPorts], PublicKey = target.PublicKey, IpVersion = IpVersions.V6 });
|
|
}
|
|
}
|
|
|
|
lock (_gate)
|
|
{
|
|
if (result.Version < _version || (result.Version == _version && Same(next, _targets)))
|
|
{
|
|
return;
|
|
}
|
|
|
|
_version = result.Version;
|
|
_targets = next;
|
|
}
|
|
|
|
try
|
|
{
|
|
_store.Write(FileName, JsonSerializer.SerializeToUtf8Bytes(new DialCache { Version = result.Version, Targets = next }, AgentConfigContext.Default.DialCache));
|
|
}
|
|
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
|
{
|
|
_log.Warn("could not save dial_targets.json", ("err", ex.Message));
|
|
}
|
|
|
|
_log.Info("dial targets updated", ("version", result.Version), ("targets", next.Count));
|
|
Changed?.Invoke();
|
|
}
|
|
|
|
private static bool Same(List<DialEndpoint> a, List<DialEndpoint> b) =>
|
|
a.Count == b.Count && a.Zip(b).All(p => Equal(p.First, p.Second));
|
|
|
|
public static bool Equal(DialEndpoint a, DialEndpoint b) =>
|
|
a.NodeId == b.NodeId && a.IpVersion == b.IpVersion && a.Host == b.Host && a.TcpPort == b.TcpPort && a.QuicPorts.SequenceEqual(b.QuicPorts) && a.PublicKey == b.PublicKey;
|
|
}
|