using System.Text.Json; using Fleet.Core; using Fleet.Hosting; using MoShiro.Protocol; namespace MoShiro.Agent; /// /// 本节点要拨的监听节点列表:由面板作为期望状态(dial_targets)直接下发;面板连不上时用上一次保存的 dial_targets.json。 /// 列表变化时用 写回 agents.json(这是配置,不是测量数据;只在变化时写,保护路由器的闪存)。 /// 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 _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)); } } } /// 列表变化(调和连接)。 public event Action? Changed; public long Version { get { lock (_gate) { return _version; } } } public IReadOnlyList Targets { get { lock (_gate) { return [.. _targets]; } } } /// 应用面板下发的列表。版本不比当前新的忽略(乱序到达的旧推送)。 public void Apply(DialTargets result) { var next = new List(); _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 a, List 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; }