using System.Net; using System.Text.Json; using Fleet.Core; using MoShiro.Protocol; namespace MoShiro.Measurement; /// /// 数据面会话:session_id 和密钥(由点对点握手里的 ECDH 派生)。两端的数据面包都用它认证。 /// Agent 的会话还带重放窗口等状态,所以是可以继承的。 /// public class DataSession { private IDatagramConnection? _datagramConnection; public DataSession(long sessionId, byte[] key) { SessionId = sessionId; Key = key; } public long SessionId { get; } public byte[] Key { get; } /// Public node identity for diagnostics; never the data-session identifier or key. public virtual string? DiagnosticPeerId => null; public IDatagramConnection? DatagramConnection => Volatile.Read(ref _datagramConnection); public void BindDatagramConnection(IDatagramConnection connection) => Volatile.Write(ref _datagramConnection, connection); /// 对端最近一次通过认证的 UDP 源地址(监听方把单向数据发往这里;拨号方不需要)。 public virtual IPEndPoint? DatagramRemoteEndPoint => null; /// TCP 首帧的随机数只能用一次。拨号方不接受连接,所以默认放行。 public virtual bool UseNonce(ReadOnlySpan nonce) => true; } /// Unreliable QUIC datagram sender bound to one authenticated measurement session. public interface IDatagramConnection { bool Send(ReadOnlySpan datagram); } /// Transport-layer QUIC counters. These are separate from application Datagram delivery/loss. public sealed record QuicTransportSnapshot(double? PacketLossRate, ulong PacketsSent, uint CongestionEvents, uint EcnCongestionEvents, ushort PathMtu); public interface IDataSessions { DataSession? Find(long sessionId); } /// 只有一个会话(拨号方:与某个监听方的会话)。 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, } /// /// 监听方的准入策略(安全边界):重放窗口、令牌桶限速、源地址学习。积木本身不含任何策略; /// 拨号方不需要,只做 MAC 校验。任何一步失败都是静默丢弃并计数,不回复。 /// public interface IAdmission { /// UDP 包: 是认证通过的源地址(实现可以据此学习 NAT 重绑定)。 Admission AdmitDatagram(DataSession session, in MeasurementDatagramHeader header, SocketAddress source, long nowUs); /// TCP 回显的一条消息(只限速)。 bool AdmitMessage(DataSession session, uint streamId, long nowUs); } /// 数据面计数(只有计数,没有主机信息):用于心跳和面板上的节点状态。 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); } /// 一个列块属于线路任务还是按需测试的某个阶段。 public sealed record FragmentScope(TaskId? Task, TestId? Test, int? Phase); /// /// 积木交出一侧的行的地方。拨号方:放进发给 Agent 的发件箱(列块);Agent 自己运行的积木(目标任务):直接盖章上传。 /// public interface IFragmentSink { void Ship(string kind, FragmentScope scope, IReadOnlyList streams, IReadOnlyDictionary? parameters, long startUtcMs, int durationMs, int port); } public enum FlushMode { /// 只交出已经稳定的行(回显类等过宽限期的)。 Settled, /// 交出全部(阶段结束或角色停止)。 All, } /// 积木无法继续(例如监听方等不到对端的地址):由编排者决定整个测试怎么结束。 public sealed class BlockFaultException : Exception { public BlockFaultException(string message) : base(message) { } } /// /// 一个运行中的积木角色。同一种采集方法的两端是一对角色(拨号方和监听方),写在同一个文件里, /// 共用包的构造、校验、行的记录;它们只产生原始列,不做任何统计,也不知道上层的方案或协议。 /// public interface IBlock { /// 运行到取消(回显、保持)或到自己结束(单向数据流的发送端)。 Task RunAsync(CancellationToken ct); /// /// 拨号方把已完成的行切成列块交给 ,返回块数;监听方返回 0(它的行留在 里等合并)。 /// :对端是行的主导方时,对端产生的行数(单向数据流的发送包数)。 /// int Flush(FlushMode mode, long peerProduced = 0); /// 这一侧作为主导方产生的行数(发出的包数)。 long Produced { get; } /// 这一侧为合并保留的按键记录;没有则为 null。 RowStore? Records(string streamName); } /// 运行一个积木所需的全部参数:Kind、已经校验并补全默认值的参数、流编号、端口、列块归属。 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 double Number(string name) => Params.GetNumber(name); public bool Bool(string name) => Params.GetBool(name); public string Text(string name) => Params.GetString(name); /// 参数或端口变化时调和器重启这个积木(不含流编号:Agent 的流编号每次重建都会重新分配)。 public string ConfigSignature => KindId + "|" + Port + "|" + string.Join(',', Params.Values.OrderBy(p => p.Key, StringComparer.Ordinal).Select(p => p.Key + "=" + p.Value.GetRawText())); /// 参数、端口或流编号变化时重启:拨号方 据此判断 Agent 下发的任务有没有变(Agent 重启后流编号会变)。 public string Signature => StreamId + "|" + ConfigSignature; /// 校验参数(补全默认值)后构造;Kind 不存在或参数非法返回 null。 public static BlockSpec? Create(string kindId, IReadOnlyDictionary 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; } } /// 积木运行的环境:时钟、套接字、对端地址、列块出口。拨号方和监听方各自组装自己用得到的部分。 public sealed class BlockEnv { public required MonoClock Clock { get; init; } public required INodeLog Log { get; init; } /// 当前的数据面会话(拨号方固定;监听方是 Agent 当前的 拨号方 会话,可能没有)。 public required Func Session { get; init; } public QuicDatagramTransport? Datagrams { get; init; } /// 拨号方:这条线路在每个时刻(单调时钟,微秒)丢包的历史概率(0 到 1),自适应探测用它在高发时段提前升频;没有历史为 null。 public Func? LossPrior { get; init; } /// 监听方的准入策略(限速等);拨号方为 null。 public IAdmission? Admission { get; init; } public DataPlaneCounters Counters { get; init; } = new(); /// 监听方:接受 TCP 数据连接。 public TcpAcceptor? TcpListen { get; init; } /// 拨号方:对端(Agent,或目标任务的目标地址)。 public IPAddress? PeerAddress { get; init; } /// 单个 Agent 任务的目标原文(http 是 URL,其余是主机[:端口]);线路任务为 null。 public string? Target { get; init; } /// 单个 Agent 任务:解析出来的地址能不能连(本地 allowed_target_cidrs);null 表示不限制。 public Func? AllowAddress { get; init; } /// 自适应频率的状态变化(用于审计);null 表示不记录。 public Action? OnRateChange { get; init; } /// 拨号方:对端 TLS 证书的 SPKI 指纹;null 表示不校验证书(目标任务,目标是任意地址)。 public byte[]? PeerSpkiSha256 { get; init; } /// 拨号方:和对端(Agent)的配对敲门密钥;每条 TCP 连接建立后先发敲门包。null 表示对端不要求敲门(目标任务的任意地址)。 public byte[]? KnockKey { get; init; } /// 拨号方:列块出口。 public IFragmentSink? Fragments { get; init; } /// 本地硬上限:批量传输(tcp_transfer)发送端的码率(Mbit/s)。批量传输会尽可能占满链路,这是防止把小机器打满的保险。 public double MaxBulkMbps { get; init; } = 100; /// 本地硬上限:单向数据流接收缓冲的包速率上界。 public long MaxPacketsPerSecond { get; init; } = 20_000; public TimeProvider Time => Clock.Time; /// 同一套环境,但绑定到一个固定的数据面会话(Agent 为每个 拨号方 会话各起一组监听方积木)。 public BlockEnv WithSession(DataSession session) => new() { Clock = Clock, Log = Log, Session = () => session, Datagrams = Datagrams, LossPrior = LossPrior, Admission = Admission, Counters = Counters, TcpListen = TcpListen, PeerAddress = PeerAddress, Target = Target, AllowAddress = AllowAddress, OnRateChange = OnRateChange, PeerSpkiSha256 = PeerSpkiSha256, KnockKey = KnockKey, Fragments = Fragments, MaxBulkMbps = MaxBulkMbps, MaxPacketsPerSecond = MaxPacketsPerSecond, }; }