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]
275 lines
11 KiB
C#
275 lines
11 KiB
C#
using System.Text.Json;
|
||
using Fleet.Hosting;
|
||
using Fleet.Core;
|
||
using Fleet.Rpc;
|
||
using MoShiro.Measurement;
|
||
using MoShiro.Protocol;
|
||
|
||
namespace MoShiro.Agent;
|
||
|
||
/// <summary>
|
||
/// 期望任务集(<c>task_set</c>)的调和器:每个任务先经过 <see cref="BudgetGuard"/>(参数校验 + 本地硬上限)。
|
||
/// 通过的线路任务<b>为每个 Probe 会话各登记一组流</b>(流编号和监听方积木都属于会话,互不干扰),并汇总成 <c>line_task_set</c> 下发给这个 Probe;
|
||
/// 通过的目标任务在本地运行,与 Probe 无关。被拒绝的任务带着原因(<c>rejected[]</c>)回报给 Backend,其余任务照常运行。
|
||
/// 每次调用都应用完整的快照,所以是幂等的。预算(码率、包速率)按单个 Probe 会话计算,会话总数由 <c>PeerListenerOptions.MaxSessions</c> 限制。
|
||
/// </summary>
|
||
public sealed class TaskManager
|
||
{
|
||
// 线路任务 = 有监听方积木、并且不是只用于按需测试的 Kind(udp_train、tcp_bulk 会占满链路);目标任务 = 可以由 Agent 自己运行的 Kind
|
||
private static readonly HashSet<string> s_lineKinds = [.. BlockCatalog.All.Where(p => p.Listener is not null && !p.OnDemandOnly).Select(p => p.Kind)];
|
||
private static readonly HashSet<string> s_targetKinds = [.. BlockCatalog.All.Where(p => p.RunsAsTarget).Select(p => p.Kind)];
|
||
|
||
private readonly object _gate = new();
|
||
private readonly Func<BudgetGuard> _guardFactory;
|
||
private readonly StreamRegistry _streams;
|
||
private readonly BlockEnv _env;
|
||
private readonly TargetRunners _targets;
|
||
private readonly Func<IReadOnlyList<int>> _udpPorts;
|
||
private readonly Func<IReadOnlyList<int>> _tcpPorts;
|
||
private readonly TrustAnchor _trust;
|
||
private readonly Dictionary<ProbeSession, Dictionary<string, LineEntry>> _sessionLines = [];
|
||
private volatile Dictionary<string, byte[]> _peers = new(StringComparer.Ordinal);
|
||
private Dictionary<string, (TaskSpec Task, TaskDecision Decision)> _lineSpecs = [];
|
||
private long _version;
|
||
private BudgetGuard? _guard;
|
||
|
||
public TaskManager(Func<BudgetGuard> guardFactory, StreamRegistry streams, BlockEnv env, TargetRunners targets, Func<IReadOnlyList<int>> udpPorts, Func<IReadOnlyList<int>> tcpPorts, TrustAnchor? trust = null)
|
||
{
|
||
_trust = trust ?? TrustAnchor.None;
|
||
_guardFactory = guardFactory;
|
||
_streams = streams;
|
||
_env = env;
|
||
_targets = targets;
|
||
_udpPorts = udpPorts;
|
||
_tcpPorts = tcpPorts;
|
||
}
|
||
|
||
public BudgetGuard Guard => _guard ??= _guardFactory();
|
||
|
||
/// <summary>某个 Probe 的线路任务集变化(版本递增):由 <see cref="PeerHub"/> 推送给这个 Probe。</summary>
|
||
public Action<ProbeSession, LineTaskSet>? OnLineTasksChanged { get; set; }
|
||
|
||
/// <summary>允许连接的 Probe 变了(期望状态里的 peers):已经不在名单里的会话由 <see cref="PeerHub"/> 关闭。</summary>
|
||
public Action<IReadOnlySet<string>>? OnPeersChanged { get; set; }
|
||
|
||
/// <summary>点对点握手时查找 Probe 的公钥(SPKI)。</summary>
|
||
public byte[]? LookupPeerKey(string peerId) => _peers.TryGetValue(peerId, out byte[]? key) ? key : null;
|
||
|
||
/// <summary>名册里全部 Probe 的公钥:敲门关卡用它们派生配对密钥。</summary>
|
||
public IReadOnlyList<byte[]> PeerKeys() => [.. _peers.Values];
|
||
|
||
/// <summary>一个新的 Probe 会话开始:为它登记全部线路任务的流并启动监听方积木,返回要下发给它的线路任务集。</summary>
|
||
public LineTaskSet OpenSession(ProbeSession probe)
|
||
{
|
||
lock (_gate)
|
||
{
|
||
var entries = new Dictionary<string, LineEntry>();
|
||
foreach ((string id, (TaskSpec task, TaskDecision decision)) in _lineSpecs)
|
||
{
|
||
LineEntry entry = BuildLineEntry(task, decision, probe);
|
||
Start(entry);
|
||
entries[id] = entry;
|
||
}
|
||
|
||
_sessionLines[probe] = entries;
|
||
return SetFor(entries);
|
||
}
|
||
}
|
||
|
||
/// <summary>Probe 会话结束:释放它的所有流和积木(记录随之丢弃,下一个会话从零开始)。</summary>
|
||
public async Task CloseSessionAsync(ProbeSession probe)
|
||
{
|
||
List<LineEntry> entries;
|
||
lock (_gate)
|
||
{
|
||
if (!_sessionLines.Remove(probe, out Dictionary<string, LineEntry>? lines))
|
||
{
|
||
return;
|
||
}
|
||
|
||
entries = [.. lines.Values];
|
||
}
|
||
|
||
foreach (LineEntry entry in entries)
|
||
{
|
||
await Retire(entry);
|
||
}
|
||
}
|
||
|
||
public async ValueTask<StateSyncResult> ApplyAsync(long version, TaskSet set, CancellationToken ct)
|
||
{
|
||
_ = ct;
|
||
var result = new StateSyncResult { AppliedVersion = version };
|
||
|
||
// 固定了操作员创世的节点:面板带来的新版本操作员集合,从固定的创世验过才采用(名册本身由面板决定)
|
||
_trust.Update(set.TrustChain);
|
||
var peers = new Dictionary<string, byte[]>(StringComparer.Ordinal);
|
||
foreach (PeerSpec peer in set.Peers)
|
||
{
|
||
try
|
||
{
|
||
peers[peer.ProbeId.Value] = Convert.FromBase64String(peer.PublicKey);
|
||
}
|
||
catch (FormatException)
|
||
{
|
||
result.Rejected.Add(new RejectedItem { Id = peer.ProbeId.Value, Reason = RejectReasons.InvalidParams });
|
||
}
|
||
}
|
||
|
||
_peers = peers;
|
||
|
||
BudgetGuard guard = Guard;
|
||
double usedMbps = 0;
|
||
long usedPps = 0;
|
||
var line = new Dictionary<string, (TaskSpec Task, TaskDecision Decision)>();
|
||
var targets = new List<(TaskSpec Spec, TaskDecision Decision)>();
|
||
|
||
foreach (TaskSpec task in set.Tasks)
|
||
{
|
||
if (!task.Enabled)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
bool shapeOk = task.Scope == "target" ? s_targetKinds.Contains(task.Kind) : s_lineKinds.Contains(task.Kind);
|
||
TaskDecision decision = shapeOk ? guard.Check(task, ref usedMbps, ref usedPps) : TaskDecision.Reject(RejectReasons.Unsupported);
|
||
if (!decision.Accepted)
|
||
{
|
||
result.Rejected.Add(new RejectedItem { Id = task.Id.Value, Reason = decision.Reason! });
|
||
continue;
|
||
}
|
||
|
||
if (task.Scope == "target")
|
||
{
|
||
targets.Add((task, decision));
|
||
}
|
||
else
|
||
{
|
||
line[task.Id.Value] = (task, decision);
|
||
}
|
||
}
|
||
|
||
_targets.Reconcile(targets);
|
||
|
||
// 参数没有变化的任务沿用原来的流和积木,数据面上的会话(重放窗口、记录)不被打断
|
||
var retired = new List<LineEntry>();
|
||
var changes = new List<(ProbeSession Probe, LineTaskSet Set)>();
|
||
lock (_gate)
|
||
{
|
||
_lineSpecs = line;
|
||
_version = version;
|
||
foreach ((ProbeSession probe, Dictionary<string, LineEntry> current) in _sessionLines)
|
||
{
|
||
var next = new Dictionary<string, LineEntry>();
|
||
foreach ((string id, (TaskSpec task, TaskDecision decision)) in line)
|
||
{
|
||
LineEntry candidate = BuildLineEntry(task, decision, probe);
|
||
if (current.TryGetValue(id, out LineEntry? existing) && existing.Signature == candidate.Signature)
|
||
{
|
||
next[id] = existing;
|
||
}
|
||
else
|
||
{
|
||
Start(candidate);
|
||
next[id] = candidate;
|
||
}
|
||
}
|
||
|
||
foreach ((string id, LineEntry entry) in current)
|
||
{
|
||
if (!next.TryGetValue(id, out LineEntry? kept) || !ReferenceEquals(kept, entry))
|
||
{
|
||
retired.Add(entry);
|
||
}
|
||
}
|
||
|
||
current.Clear();
|
||
foreach ((string id, LineEntry entry) in next)
|
||
{
|
||
current[id] = entry;
|
||
}
|
||
|
||
changes.Add((probe, SetFor(current)));
|
||
}
|
||
}
|
||
|
||
foreach (LineEntry entry in retired)
|
||
{
|
||
await Retire(entry);
|
||
}
|
||
|
||
OnPeersChanged?.Invoke(new HashSet<string>(peers.Keys, StringComparer.Ordinal));
|
||
foreach ((ProbeSession probe, LineTaskSet next) in changes)
|
||
{
|
||
OnLineTasksChanged?.Invoke(probe, next);
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
/// <summary>停止时释放所有 Probe 会话的流和所有目标任务(Backend 失联的时候任务继续运行:节点自治)。</summary>
|
||
public async Task StopAsync()
|
||
{
|
||
_targets.Reconcile([]);
|
||
List<LineEntry> entries;
|
||
lock (_gate)
|
||
{
|
||
entries = [.. _sessionLines.Values.SelectMany(l => l.Values)];
|
||
_sessionLines.Clear();
|
||
}
|
||
|
||
foreach (LineEntry entry in entries)
|
||
{
|
||
await Retire(entry);
|
||
}
|
||
}
|
||
|
||
private LineTaskSet SetFor(Dictionary<string, LineEntry> entries) =>
|
||
new()
|
||
{
|
||
Version = _version,
|
||
Tasks = [.. entries.Values.Select(e => e.Task)],
|
||
UdpPorts = [.. _udpPorts()],
|
||
TcpPorts = [.. _tcpPorts()],
|
||
};
|
||
|
||
private LineEntry BuildLineEntry(TaskSpec task, TaskDecision decision, ProbeSession probe)
|
||
{
|
||
var info = new StreamInfo(_streams.Allocate(), task.Kind, decision.Resolved!) { TaskId = task.Id, Owner = probe };
|
||
var lineTask = new LineTask
|
||
{
|
||
Id = task.Id,
|
||
Kind = task.Kind,
|
||
Params = new Dictionary<string, JsonElement>(decision.Resolved!.Values),
|
||
StreamId = info.Id,
|
||
Port = info.Port,
|
||
};
|
||
var spec = new BlockSpec(decision.Kind!, decision.Resolved, info.Id, info.Port, new FragmentScope(task.Id, null, null));
|
||
return new LineEntry(lineTask, info, spec, _env.WithSession(probe));
|
||
}
|
||
|
||
/// <summary>在数据面登记这个流并启动它的监听方积木。</summary>
|
||
private void Start(LineEntry entry)
|
||
{
|
||
IBlock block = BlockCatalog.Find(entry.Stream.Kind)!.Listener!(entry.Env, entry.Spec);
|
||
entry.Stream.Running = RunningBlock.Start(block, entry.Signature);
|
||
_streams.Add(entry.Stream);
|
||
}
|
||
|
||
private async Task Retire(LineEntry entry)
|
||
{
|
||
_streams.Remove(entry.Stream.Id);
|
||
if (entry.Stream.Running is { } running)
|
||
{
|
||
await running.StopAsync();
|
||
}
|
||
|
||
entry.Stream.Owner?.ForgetStream(entry.Stream.Id);
|
||
}
|
||
|
||
private sealed record LineEntry(LineTask Task, StreamInfo Stream, BlockSpec Spec, BlockEnv Env)
|
||
{
|
||
public string Signature => Spec.ConfigSignature;
|
||
}
|
||
}
|