Files
MoShiro/backend/shared/MoShiro.Measurement/Core/Contracts.cs
T
2026-10-07 18:56:40 +08:00

253 lines
9.6 KiB
C#

using System.Net;
using System.Text.Json;
using Fleet.Core;
using MoShiro.Protocol;
namespace MoShiro.Measurement;
/// <summary>
/// 数据面会话:<c>session_id</c> 和密钥(由点对点握手里的 ECDH 派生)。两端的数据面包都用它认证。
/// Agent 的会话还带重放窗口等状态,所以是可以继承的。
/// </summary>
public class DataSession
{
public DataSession(long sessionId, byte[] key)
{
SessionId = sessionId;
Key = key;
}
public long SessionId { get; }
public byte[] Key { get; }
/// <summary>对端最近一次通过认证的 UDP 源地址(监听方把单向数据发往这里;拨号方不需要)。</summary>
public virtual IPEndPoint? UdpEndpoint => null;
/// <summary>TCP 首帧的随机数只能用一次。拨号方不接受连接,所以默认放行。</summary>
public virtual bool UseNonce(ReadOnlySpan<byte> nonce) => true;
}
public interface IDataSessions
{
DataSession? Find(long sessionId);
}
/// <summary>只有一个会话(拨号方:Probe 与某个 Agent 的会话)。</summary>
public sealed class SingleSession : IDataSessions
{
private readonly DataSession _session;
public SingleSession(DataSession session)
{
_session = session;
}
public DataSession? Find(long sessionId) => sessionId == _session.SessionId ? _session : null;
}
public enum Admission
{
Accepted,
Invalid,
RateLimited,
}
/// <summary>
/// 监听方的准入策略(安全边界):重放窗口、令牌桶限速、源地址学习。积木本身不含任何策略;
/// 拨号方不需要,只做 MAC 校验。任何一步失败都是静默丢弃并计数,不回复。
/// </summary>
public interface IAdmission
{
/// <summary>UDP 包:<paramref name="source"/> 是认证通过的源地址(实现可以据此学习 NAT 重绑定)。</summary>
Admission AdmitPacket(DataSession session, in UdpHeader header, SocketAddress source, long nowUs);
/// <summary>TCP 回显的一条消息(只限速)。</summary>
bool AdmitMessage(DataSession session, uint streamId, long nowUs);
}
/// <summary>数据面计数(只有计数,没有主机信息):用于心跳和面板上的节点状态。</summary>
public sealed class DataPlaneCounters
{
private long _packetsIn;
private long _packetsOut;
private long _invalid;
private long _rateLimited;
public long PacketsIn => Interlocked.Read(ref _packetsIn);
public long PacketsOut => Interlocked.Read(ref _packetsOut);
public long DroppedInvalid => Interlocked.Read(ref _invalid);
public long DroppedRateLimited => Interlocked.Read(ref _rateLimited);
public void PacketIn() => Interlocked.Increment(ref _packetsIn);
public void PacketOut() => Interlocked.Increment(ref _packetsOut);
public void DropInvalid() => Interlocked.Increment(ref _invalid);
public void DropRateLimited() => Interlocked.Increment(ref _rateLimited);
}
/// <summary>一个列块属于线路任务还是按需测试的某个阶段。</summary>
public sealed record FragmentScope(TaskId? Task, TestId? Test, int? Phase);
/// <summary>
/// 积木交出一侧的行的地方。Probe:放进发给 Agent 的发件箱(列块);Agent 自己运行的积木(目标任务):直接盖章上传。
/// </summary>
public interface IFragmentSink
{
void Ship(string kind, FragmentScope scope, IReadOnlyList<RawStream> streams, IReadOnlyDictionary<string, JsonElement>? parameters, long startUtcMs, int durationMs, int port);
}
public enum FlushMode
{
/// <summary>只交出已经稳定的行(回显类等过宽限期的)。</summary>
Settled,
/// <summary>交出全部(阶段结束或角色停止)。</summary>
All,
}
/// <summary>积木无法继续(例如监听方等不到对端的地址):由编排者决定整个测试怎么结束。</summary>
public sealed class BlockFaultException : Exception
{
public BlockFaultException(string message)
: base(message)
{
}
}
/// <summary>
/// 一个运行中的积木角色。同一种采集方法的两端是一对角色(拨号方 = Probe,监听方 = Agent),写在同一个文件里,
/// 共用包的构造、校验、行的记录;它们只产生<b>原始列</b>,不做任何统计,也不知道上层的方案或协议。
/// </summary>
public interface IBlock
{
/// <summary>运行到取消(回显、保持)或到自己结束(单向数据流的发送端)。</summary>
Task RunAsync(CancellationToken ct);
/// <summary>
/// 拨号方把已完成的行切成列块交给 <see cref="IFragmentSink"/>,返回块数;监听方返回 0(它的行留在 <see cref="Records"/> 里等合并)。
/// <paramref name="peerProduced"/>:对端是行的主导方时,对端产生的行数(单向数据流的发送包数)。
/// </summary>
int Flush(FlushMode mode, long peerProduced = 0);
/// <summary>这一侧作为主导方产生的行数(发出的包数)。</summary>
long Produced { get; }
/// <summary>这一侧为合并保留的按键记录;没有则为 null。</summary>
RowStore? Records(string streamName);
}
/// <summary>运行一个积木所需的全部参数:Kind、已经校验并补全默认值的参数、流编号、端口、列块归属。</summary>
public sealed class BlockSpec
{
public BlockSpec(KindSpec kind, ParamValues parameters, uint streamId, int port, FragmentScope scope)
{
Kind = kind;
Params = parameters;
StreamId = streamId;
Port = port;
Scope = scope;
}
public KindSpec Kind { get; }
public ParamValues Params { get; }
public uint StreamId { get; }
public int Port { get; }
public FragmentScope Scope { get; }
public string KindId => Kind.Id;
public long Int(string name) => Params.GetInt(name);
public bool Bool(string name) => Params.GetBool(name);
/// <summary>参数或端口变化时调和器重启这个积木(不含流编号:Agent 的流编号每次重建都会重新分配)。</summary>
public string ConfigSignature => KindId + "|" + Port + "|" +
string.Join(',', Params.Values.OrderBy(p => p.Key, StringComparer.Ordinal).Select(p => p.Key + "=" + p.Value.GetRawText()));
/// <summary>参数、端口或流编号变化时重启:Probe 据此判断 Agent 下发的任务有没有变(Agent 重启后流编号会变)。</summary>
public string Signature => StreamId + "|" + ConfigSignature;
/// <summary>校验参数(补全默认值)后构造;Kind 不存在或参数非法返回 null。</summary>
public static BlockSpec? Create(string kindId, IReadOnlyDictionary<string, JsonElement> parameters, uint streamId, int port, FragmentScope scope)
{
KindSpec? kind = Kinds.Find(kindId);
if (kind is null)
{
return null;
}
ParamResolution resolved = ParamResolver.Resolve(kind.Params, parameters);
return resolved.IsValid ? new BlockSpec(kind, resolved.Values!, streamId, port, scope) : null;
}
}
/// <summary>积木运行的环境:时钟、套接字、对端地址、列块出口。拨号方和监听方各自组装自己用得到的部分。</summary>
public sealed class BlockEnv
{
public required MonoClock Clock { get; init; }
public required INodeLog Log { get; init; }
/// <summary>当前的数据面会话(拨号方固定;监听方是 Agent 当前的 Probe 会话,可能没有)。</summary>
public required Func<DataSession?> Session { get; init; }
public UdpMux? Udp { get; init; }
/// <summary>监听方的准入策略(限速等);拨号方为 null。</summary>
public IAdmission? Admission { get; init; }
public DataPlaneCounters Counters { get; init; } = new();
/// <summary>监听方:接受 TCP 数据连接。</summary>
public TcpAcceptor? TcpListen { get; init; }
/// <summary>拨号方:对端(Agent,或目标任务的目标地址)。</summary>
public IPAddress? PeerAddress { get; init; }
/// <summary>拨号方:对端 TLS 证书的 SPKI 指纹;null 表示不校验证书(目标任务,目标是任意地址)。</summary>
public byte[]? PeerSpkiSha256 { get; init; }
/// <summary>拨号方:和对端(Agent)的配对敲门密钥;每条 TCP 连接建立后先发敲门包。null 表示对端不要求敲门(目标任务的任意地址)。</summary>
public byte[]? KnockKey { get; init; }
/// <summary>拨号方:列块出口。</summary>
public IFragmentSink? Fragments { get; init; }
/// <summary>本地硬上限:批量传输(tcp_bulk)发送端的码率(Mbit/s)。批量传输会尽可能占满链路,这是防止把小机器打满的保险。</summary>
public double MaxBulkMbps { get; init; } = 100;
/// <summary>本地硬上限:单向数据流接收缓冲的包速率上界。</summary>
public long MaxPacketsPerSecond { get; init; } = 20_000;
public TimeProvider Time => Clock.Time;
/// <summary>同一套环境,但绑定到一个固定的数据面会话(Agent 为每个 Probe 会话各起一组监听方积木)。</summary>
public BlockEnv WithSession(DataSession session) =>
new()
{
Clock = Clock,
Log = Log,
Session = () => session,
Udp = Udp,
Admission = Admission,
Counters = Counters,
TcpListen = TcpListen,
PeerAddress = PeerAddress,
PeerSpkiSha256 = PeerSpkiSha256,
KnockKey = KnockKey,
Fragments = Fragments,
MaxBulkMbps = MaxBulkMbps,
MaxPacketsPerSecond = MaxPacketsPerSecond,
};
}