refactor!: rename the project from Monitor to MoShiro [CI]
ci/woodpecker/push/main-amd64-build Pipeline was successful
ci/woodpecker/push/main-amd64-build Pipeline was successful
Namespaces, assemblies, solution, binaries, images, systemd units, config paths, environment variables, protocol methods and the Linux user are all renamed. Breaking: existing databases, node identities and installed units must be recreated.
This commit is contained in:
1 parent
27ac7fb787
commit
2e146d4da7
309 files changed
+1565
-1572
No files matched your search
@@ -0,0 +1,333 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using Fleet.Core;
|
||||
using Fleet.Link;
|
||||
using Fleet.Rpc;
|
||||
using MoShiro.Measurement;
|
||||
using MoShiro.Protocol;
|
||||
|
||||
namespace MoShiro.Agent;
|
||||
|
||||
/// <summary>进程内计数(只有计数,没有主机信息):用于心跳和面板上的节点状态。数据面的包计数由共享的 <see cref="DataPlaneCounters"/> 提供。</summary>
|
||||
public sealed class AgentCounters
|
||||
{
|
||||
private long _fragments;
|
||||
private long _fragmentsDiscarded;
|
||||
|
||||
public DataPlaneCounters Data { get; } = new();
|
||||
|
||||
public long PacketsIn => Data.PacketsIn;
|
||||
|
||||
public long PacketsOut => Data.PacketsOut;
|
||||
|
||||
public long DroppedInvalid => Data.DroppedInvalid;
|
||||
|
||||
public long DroppedRateLimited => Data.DroppedRateLimited;
|
||||
|
||||
public long Fragments => Interlocked.Read(ref _fragments);
|
||||
|
||||
public long FragmentsDiscarded => Interlocked.Read(ref _fragmentsDiscarded);
|
||||
|
||||
public void FragmentAccepted() => Interlocked.Increment(ref _fragments);
|
||||
|
||||
public void FragmentDiscarded() => Interlocked.Increment(ref _fragmentsDiscarded);
|
||||
}
|
||||
|
||||
public sealed record PeerContext(ProbeSession Session, MonoClock Clock);
|
||||
|
||||
/// <summary>
|
||||
/// 一个已经认证的 Probe 会话:数据面的 <c>session_id</c> 和密钥(由点对点握手里的 ECDH 派生)、最近一次的 UDP 源地址、
|
||||
/// 每个流的重放窗口。数据面的包不绑定五元组,NAT 映射变化之后自动跟随新的源地址。
|
||||
/// </summary>
|
||||
public sealed class ProbeSession : DataSession
|
||||
{
|
||||
private readonly ConcurrentDictionary<uint, ReplayWindow> _windows = new();
|
||||
private readonly ConcurrentDictionary<ulong, byte> _nonces = new();
|
||||
private SocketAddress? _udpSource;
|
||||
|
||||
public ProbeSession(PeerSessionInfo info)
|
||||
: base(info.Credentials.SessionIdValue, info.Credentials.Key)
|
||||
{
|
||||
Info = info;
|
||||
}
|
||||
|
||||
public PeerSessionInfo Info { get; }
|
||||
|
||||
public string ProbeId => Info.PeerId;
|
||||
|
||||
/// <summary>Probe 的程序版本(握手里自报,只用于展示)。</summary>
|
||||
public string? Version => Info.Version;
|
||||
|
||||
/// <summary>Probe 的远端地址(经过 NAT 之后的公网 IP)。</summary>
|
||||
public string? RemoteAddress => (Info.Remote as IPEndPoint)?.Address.ToString();
|
||||
|
||||
public long ConnectedAtUtcMs { get; init; }
|
||||
|
||||
public RpcSession<PeerContext>? Rpc { get; set; }
|
||||
|
||||
/// <summary>最近一次认证通过的 UDP 包的源地址(Agent 把单向数据发往这里)。</summary>
|
||||
public override IPEndPoint? UdpEndpoint
|
||||
{
|
||||
get
|
||||
{
|
||||
SocketAddress? source = Volatile.Read(ref _udpSource);
|
||||
return source is null ? null : (IPEndPoint)new IPEndPoint(IPAddress.Any, 0).Create(source);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>记录 UDP 源地址;NAT 映射变化后地址变了返回 true(上报 endpoint_changed)。第一次看到地址不算变化。</summary>
|
||||
public bool UpdateSource(SocketAddress source)
|
||||
{
|
||||
SocketAddress? previous = Volatile.Read(ref _udpSource);
|
||||
if (previous is not null && previous.Equals(source))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// SocketAddress 的缓冲区会被接收循环复用,所以必须复制
|
||||
var copy = new SocketAddress(source.Family, source.Size);
|
||||
for (int i = 0; i < source.Size; i++)
|
||||
{
|
||||
copy[i] = source[i];
|
||||
}
|
||||
|
||||
Volatile.Write(ref _udpSource, copy);
|
||||
return previous is not null;
|
||||
}
|
||||
|
||||
public bool AcceptSequence(uint streamId, uint seq)
|
||||
{
|
||||
ReplayWindow window = _windows.GetOrAdd(streamId, _ => new ReplayWindow());
|
||||
lock (window)
|
||||
{
|
||||
return window.TryAccept(seq);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>TCP 首帧的随机数只能用一次,防止被截获之后重放。</summary>
|
||||
public override bool UseNonce(ReadOnlySpan<byte> nonce)
|
||||
{
|
||||
ulong hash = 14695981039346656037UL; // FNV-1a:随机数本身已经是 128 位随机,这里只需要一个去重用的指纹
|
||||
foreach (byte b in nonce)
|
||||
{
|
||||
hash = (hash ^ b) * 1099511628211UL;
|
||||
}
|
||||
|
||||
if (_nonces.Count > 4096)
|
||||
{
|
||||
_nonces.Clear();
|
||||
}
|
||||
|
||||
return _nonces.TryAdd(hash, 0);
|
||||
}
|
||||
|
||||
public void ForgetStream(uint streamId) => _windows.TryRemove(streamId, out _);
|
||||
}
|
||||
|
||||
/// <summary>当前连着的 Probe 会话:每个 Probe 一个;同一个 Probe 重连时新会话取代它自己的旧会话。</summary>
|
||||
public sealed class SessionRegistry : IDataSessions
|
||||
{
|
||||
private readonly object _gate = new();
|
||||
private readonly Dictionary<string, ProbeSession> _byProbe = new(StringComparer.Ordinal);
|
||||
private readonly Dictionary<long, ProbeSession> _bySession = [];
|
||||
|
||||
public IReadOnlyList<ProbeSession> All
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
return [.. _byProbe.Values];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ProbeSession? Get(string probeId)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
return _byProbe.GetValueOrDefault(probeId);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>登记新会话;返回同一个 Probe 的旧会话(如果有)。</summary>
|
||||
public ProbeSession? Replace(ProbeSession session)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
_byProbe.TryGetValue(session.ProbeId, out ProbeSession? old);
|
||||
if (old is not null)
|
||||
{
|
||||
_bySession.Remove(old.SessionId);
|
||||
}
|
||||
|
||||
_byProbe[session.ProbeId] = session;
|
||||
_bySession[session.SessionId] = session;
|
||||
return old;
|
||||
}
|
||||
}
|
||||
|
||||
public void Remove(ProbeSession session)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (_byProbe.TryGetValue(session.ProbeId, out ProbeSession? current) && ReferenceEquals(current, session))
|
||||
{
|
||||
_byProbe.Remove(session.ProbeId);
|
||||
}
|
||||
|
||||
if (_bySession.TryGetValue(session.SessionId, out ProbeSession? bySession) && ReferenceEquals(bySession, session))
|
||||
{
|
||||
_bySession.Remove(session.SessionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public DataSession? Find(long sessionId)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
return _bySession.GetValueOrDefault(sessionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>数据面上一个流(一个线路任务,或测试的一个阶段)的元数据:准入策略据此限速,合并据此找到积木的记录。</summary>
|
||||
public sealed class StreamInfo
|
||||
{
|
||||
private readonly object _bucketGate = new();
|
||||
private int _fragmentsDone;
|
||||
private double _tokens = -1;
|
||||
private long _lastRefillUs;
|
||||
|
||||
public StreamInfo(uint id, string kind, ParamValues parameters)
|
||||
{
|
||||
Id = id;
|
||||
Kind = kind;
|
||||
Params = parameters;
|
||||
KindSpec spec = Kinds.Find(kind)!;
|
||||
DeclaredPps = kind switch
|
||||
{
|
||||
"udp_echo" or "tcp_echo" => parameters.GetInt("rate_hz"),
|
||||
"udp_train" => parameters.GetNumber("mbps") * 1_000_000 / 8 / Math.Max(1, parameters.GetInt("size")),
|
||||
_ => 20,
|
||||
};
|
||||
Port = spec.Params.Any(p => p.Name == "port") ? (int)parameters.GetInt("port") : 0;
|
||||
}
|
||||
|
||||
public uint Id { get; }
|
||||
|
||||
public string Kind { get; }
|
||||
|
||||
public ParamValues Params { get; }
|
||||
|
||||
public TaskId? TaskId { get; init; }
|
||||
|
||||
public TestId? TestId { get; init; }
|
||||
|
||||
public int? Phase { get; init; }
|
||||
|
||||
/// <summary>这个流属于哪个 Probe 会话:别的会话的包即使带着这个流编号也不会被接受。</summary>
|
||||
public ProbeSession? Owner { get; init; }
|
||||
|
||||
public int Port { get; }
|
||||
|
||||
public double DeclaredPps { get; }
|
||||
|
||||
/// <summary>运行这个流的监听方积木(合并时从它取记录)。</summary>
|
||||
public RunningBlock? Running { get; set; }
|
||||
|
||||
/// <summary>该流已经处理(交给 Backend)的列块数:测试阶段据此等待全部列块到齐。</summary>
|
||||
public int FragmentsDone => _fragmentsDone;
|
||||
|
||||
public void FragmentDone() => Interlocked.Increment(ref _fragmentsDone);
|
||||
|
||||
/// <summary>令牌桶:实际速率超过声明速率的 1.2 倍(外加一点突发余量)就丢弃。</summary>
|
||||
public bool TryAdmit(long nowUs)
|
||||
{
|
||||
double rate = DeclaredPps * 1.2;
|
||||
double burst = Math.Max(10, rate * 0.25);
|
||||
lock (_bucketGate)
|
||||
{
|
||||
if (_tokens < 0)
|
||||
{
|
||||
_tokens = burst;
|
||||
_lastRefillUs = nowUs;
|
||||
}
|
||||
|
||||
_tokens = Math.Min(burst, _tokens + ((nowUs - _lastRefillUs) / 1_000_000.0 * rate));
|
||||
_lastRefillUs = nowUs;
|
||||
if (_tokens < 1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_tokens -= 1;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class StreamRegistry : IRecordSource
|
||||
{
|
||||
private readonly ConcurrentDictionary<uint, StreamInfo> _streams = new();
|
||||
private int _next;
|
||||
|
||||
public uint Allocate() => (uint)Interlocked.Increment(ref _next);
|
||||
|
||||
public void Add(StreamInfo stream) => _streams[stream.Id] = stream;
|
||||
|
||||
public void Remove(uint id) => _streams.TryRemove(id, out _);
|
||||
|
||||
public bool TryGet(uint id, out StreamInfo stream) => _streams.TryGetValue(id, out stream!);
|
||||
|
||||
public StreamInfo? FindByTask(TaskId task, ProbeSession owner) => _streams.Values.FirstOrDefault(s => s.TaskId == task && ReferenceEquals(s.Owner, owner));
|
||||
|
||||
public StreamInfo? FindByTest(TestId test, int phase) => _streams.Values.FirstOrDefault(s => s.TestId == test && s.Phase == phase);
|
||||
|
||||
public IReadOnlyCollection<StreamInfo> All => [.. _streams.Values];
|
||||
|
||||
public RowStore? Records(uint streamId, string streamName) =>
|
||||
_streams.TryGetValue(streamId, out StreamInfo? info) ? info.Running?.Block.Records(streamName) : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Agent 的准入策略(安全边界):重放窗口 → 流的令牌桶(声明速率的 1.2 倍)→ 源地址学习(NAT 重绑定自动跟随)。
|
||||
/// 积木本身不含任何策略;这里的任何一步失败,数据面都是静默丢弃并计数、不回复。
|
||||
/// </summary>
|
||||
public sealed class AgentAdmission : IAdmission
|
||||
{
|
||||
private readonly StreamRegistry _streams;
|
||||
|
||||
public AgentAdmission(StreamRegistry streams)
|
||||
{
|
||||
_streams = streams;
|
||||
}
|
||||
|
||||
/// <summary>NAT 映射变化,源地址变了:上报 <c>endpoint_changed</c>。</summary>
|
||||
public Action<ProbeSession>? OnEndpointChanged { get; set; }
|
||||
|
||||
public Admission AdmitPacket(DataSession session, in UdpHeader header, SocketAddress source, long nowUs)
|
||||
{
|
||||
if (session is not ProbeSession probe || !_streams.TryGet(header.StreamId, out StreamInfo? stream) || !ReferenceEquals(stream.Owner, probe) || !probe.AcceptSequence(header.StreamId, header.Seq))
|
||||
{
|
||||
return Admission.Invalid;
|
||||
}
|
||||
|
||||
if (!stream.TryAdmit(nowUs))
|
||||
{
|
||||
return Admission.RateLimited;
|
||||
}
|
||||
|
||||
if (probe.UpdateSource(source))
|
||||
{
|
||||
OnEndpointChanged?.Invoke(probe);
|
||||
}
|
||||
|
||||
return Admission.Accepted;
|
||||
}
|
||||
|
||||
public bool AdmitMessage(DataSession session, uint streamId, long nowUs) =>
|
||||
_streams.TryGet(streamId, out StreamInfo? stream) && ReferenceEquals(stream.Owner, session) && stream.TryAdmit(nowUs);
|
||||
}
|
||||
Reference in new issue
Block a user