measurement: name the kinds by protocol and action, add the http_request kind and QUIC settings
This commit is contained in:
1 parent
bf2a598891
commit
e77880d372
31 files changed
+938
-141
No files matched your search
@@ -10,7 +10,7 @@ public static class Kinds
|
||||
|
||||
/// <summary>这个任务可能达到的最高探测频率(Hz):自适应时是 <c>rate_max_hz</c>,否则是固定的 <c>rate_hz</c>。预算、限速和准入都按它算。</summary>
|
||||
public static long PeakRateHz(string kindId, ParamValues values) =>
|
||||
(kindId is "udp_echo" or "tcp_echo") && values.GetBool("adaptive") ? Math.Max(values.GetInt("rate_max_hz"), values.GetInt("rate_min_hz")) : values.GetInt("rate_hz");
|
||||
(kindId is "quic_ping" or "tcp_ping") && values.GetBool("adaptive") ? Math.Max(values.GetInt("rate_max_hz"), values.GetInt("rate_min_hz")) : values.GetInt("rate_hz");
|
||||
|
||||
private static readonly StreamSpec s_echoMsg = new(
|
||||
"msg",
|
||||
@@ -30,8 +30,8 @@ public static class Kinds
|
||||
],
|
||||
Union: true);
|
||||
|
||||
public static readonly KindSpec UdpEcho = new(
|
||||
"udp_echo",
|
||||
public static readonly KindSpec QuicPing = new(
|
||||
"quic_ping",
|
||||
"QUIC Datagram echo",
|
||||
"Fixed-rate QUIC Datagram echo with four timestamps per packet.",
|
||||
Scope.Line,
|
||||
@@ -47,10 +47,10 @@ public static class Kinds
|
||||
P.Int("port", 7443, 1, 65535),
|
||||
],
|
||||
[s_echoMsg],
|
||||
p => Bandwidth.UdpEcho(PeakRateHz("udp_echo", p), p.GetInt("size")));
|
||||
p => Bandwidth.UdpEcho(PeakRateHz("quic_ping", p), p.GetInt("size")));
|
||||
|
||||
public static readonly KindSpec UdpTrain = new(
|
||||
"udp_train",
|
||||
public static readonly KindSpec QuicStream = new(
|
||||
"quic_stream",
|
||||
"QUIC Datagram train",
|
||||
"Fixed-rate or laddered one-way QUIC Datagram stream (Agent to 拨号方) for loss and delay variation under load.",
|
||||
Scope.Line,
|
||||
@@ -68,8 +68,8 @@ public static class Kinds
|
||||
],
|
||||
p => (long)(p.GetNumber("mbps") * 1_000_000));
|
||||
|
||||
public static readonly KindSpec TcpEcho = new(
|
||||
"tcp_echo",
|
||||
public static readonly KindSpec TcpPing = new(
|
||||
"tcp_ping",
|
||||
"TCP echo",
|
||||
"Small TCP messages echoed with four timestamps, plus periodic kernel tcp_info samples.",
|
||||
Scope.Line,
|
||||
@@ -88,10 +88,10 @@ public static class Kinds
|
||||
P.Bool("tls", false),
|
||||
],
|
||||
[s_echoMsg, s_tcpInfo],
|
||||
p => Bandwidth.TcpEcho(PeakRateHz("tcp_echo", p), p.GetInt("max")));
|
||||
p => Bandwidth.TcpEcho(PeakRateHz("tcp_ping", p), p.GetInt("max")));
|
||||
|
||||
public static readonly KindSpec TcpBulk = new(
|
||||
"tcp_bulk",
|
||||
public static readonly KindSpec TcpTransfer = new(
|
||||
"tcp_transfer",
|
||||
"TCP bulk",
|
||||
"Bulk TCP transfer on several streams with periodic byte counters and tcp_info.",
|
||||
Scope.Line,
|
||||
@@ -110,8 +110,8 @@ public static class Kinds
|
||||
],
|
||||
_ => 0);
|
||||
|
||||
public static readonly KindSpec Connect = new(
|
||||
"connect",
|
||||
public static readonly KindSpec TcpConnect = new(
|
||||
"tcp_connect",
|
||||
"Connect",
|
||||
"Repeated TCP and TLS connection establishment with per-phase timings and outcome.",
|
||||
Scope.Line | Scope.Target,
|
||||
@@ -129,8 +129,8 @@ public static class Kinds
|
||||
],
|
||||
p => p.GetInt("per_minute") * 4000 / 60);
|
||||
|
||||
public static readonly KindSpec Hold = new(
|
||||
"hold",
|
||||
public static readonly KindSpec TlsHold = new(
|
||||
"tls_hold",
|
||||
"Hold",
|
||||
"A long-lived TLS connection with periodic heartbeats; records resets and lifetime.",
|
||||
Scope.Line,
|
||||
@@ -144,8 +144,38 @@ public static class Kinds
|
||||
],
|
||||
p => 2000 / Math.Max(1, p.GetInt("heartbeat_s")) * 8);
|
||||
|
||||
public static readonly KindSpec IcmpEcho = new(
|
||||
"icmp_echo",
|
||||
public static readonly KindSpec HttpRequest = new(
|
||||
"http_request",
|
||||
"HTTP request",
|
||||
"One Agent requests a URL over HTTP/1.1, HTTP/2 or HTTP/3 and records DNS, TCP, time to first byte, total time and status. Target is the URL.",
|
||||
Scope.Target,
|
||||
new BlockSet("http", "fixed", "fixed", "request", []),
|
||||
[
|
||||
P.Enum("version", "h2", ["h1", "h2", "h3"], "HTTP version; the request fails instead of falling back."),
|
||||
P.Enum("method", "GET", ["GET", "HEAD"]),
|
||||
P.Bool("adaptive", true, "Adapt the request rate to the failure rate: low while the target answers, up to rate_max_per_min when it fails, exponential back-off while it is unreachable."),
|
||||
P.Int("per_minute", 6, 1, 120, "/min", "Fixed rate, used when adaptive is false."),
|
||||
P.Int("rate_min_per_min", 1, 1, 120, "/min"),
|
||||
P.Int("rate_max_per_min", 12, 1, 120, "/min", "Highest rate, and the reference grid for sample weights."),
|
||||
P.Number("tau_pct", 5, 0.1, 50, "%", "Failure rate a quiet target has to be able to prove below, at 95% confidence."),
|
||||
P.Int("timeout_ms", 10000, 500, 60000, "ms"),
|
||||
P.Int("read_kb", 16, 0, 1024, "KiB", "Body bytes to read before the request counts as finished (0: headers only)."),
|
||||
P.Int("ok_status", 0, 0, 599, null, "Status that counts as success; 0 means any status below 400."),
|
||||
P.Bool("tls_verify", true),
|
||||
],
|
||||
[
|
||||
new StreamSpec(
|
||||
"req",
|
||||
[
|
||||
Col.Seq("attempt"), Col.TsDialer("ts"), Col.U16("w"),
|
||||
Col.U32("dns_us", nullable: true), Col.U32("tcp_us", nullable: true), Col.U32("ttfb_us", nullable: true), Col.U32("total_us", nullable: true),
|
||||
Col.U16("status", nullable: true), Col.U8("proto", nullable: true), Col.U8("outcome"),
|
||||
]),
|
||||
],
|
||||
p => p.GetInt("rate_max_per_min") * (2000 + p.GetInt("read_kb") * 1024) * 8 / 60);
|
||||
|
||||
public static readonly KindSpec IcmpPing = new(
|
||||
"icmp_ping",
|
||||
"ICMP echo",
|
||||
"拨号方 or Agent side ICMP echo to a target address.",
|
||||
Scope.Target,
|
||||
@@ -159,7 +189,7 @@ public static class Kinds
|
||||
],
|
||||
p => Bandwidth.UdpEcho(p.GetInt("rate_hz"), p.GetInt("size")));
|
||||
|
||||
public static readonly IReadOnlyList<KindSpec> All = [UdpEcho, UdpTrain, TcpEcho, TcpBulk, Connect, Hold, IcmpEcho];
|
||||
public static readonly IReadOnlyList<KindSpec> All = [QuicPing, QuicStream, TcpPing, TcpTransfer, TcpConnect, TlsHold, HttpRequest, IcmpPing];
|
||||
|
||||
public static KindSpec? Find(string id)
|
||||
{
|
||||
|
||||
@@ -488,7 +488,7 @@ public sealed class ControlHealth
|
||||
/// <summary>任务层:测量自己使用的链路。TCP 类积木自带套接字和重拨,QUIC 数据通道由拨号方单独维护。</summary>
|
||||
public sealed class TaskHealth
|
||||
{
|
||||
/// <summary>正在运行的 TCP 类线路积木数(tcp_echo、connect、hold、tcp_bulk)。</summary>
|
||||
/// <summary>正在运行的 TCP 类线路积木数(tcp_ping、connect、hold、tcp_transfer)。</summary>
|
||||
[JsonPropertyName("tcp_blocks")]
|
||||
public int TcpBlocks { get; set; }
|
||||
|
||||
@@ -496,7 +496,7 @@ public sealed class TaskHealth
|
||||
public QuicHealth Quic { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>QUIC 数据通道(udp_echo、udp_train 使用)的状态、握手统计和传输层统计。</summary>
|
||||
/// <summary>QUIC 数据通道(quic_ping、quic_stream 使用)的状态、握手统计和传输层统计。</summary>
|
||||
public sealed class QuicHealth
|
||||
{
|
||||
[JsonPropertyName("state")]
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
using System.Globalization;
|
||||
using System.Net;
|
||||
|
||||
namespace MoShiro.Protocol;
|
||||
|
||||
/// <summary>
|
||||
/// 单个 Agent 任务的目标:<c>http</c> 是 <c>http(s)://主机[:端口][/路径]</c>,其余 Kind 是 <c>主机[:端口]</c>(IP 或域名,IPv6 带端口时用方括号)。
|
||||
/// Backend 只检查格式,不做 DNS 解析;解析在 Agent 上进行,每个解析出的地址都要在 <c>allowed_target_cidrs</c> 里。
|
||||
/// </summary>
|
||||
public static class TargetHost
|
||||
{
|
||||
public const string HttpKind = "http_request";
|
||||
|
||||
/// <summary>解析目标;<paramref name="port"/> 为 0 表示没有写端口,<paramref name="url"/> 只在 http(s) URL 时有值。</summary>
|
||||
public static bool TryParse(string? target, out string host, out int port, out Uri? url)
|
||||
{
|
||||
host = "";
|
||||
port = 0;
|
||||
url = null;
|
||||
if (string.IsNullOrWhiteSpace(target) || target.Length > 2048 || target.Any(char.IsWhiteSpace))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (target.Contains("://", StringComparison.Ordinal))
|
||||
{
|
||||
if (!Uri.TryCreate(target, UriKind.Absolute, out Uri? uri) || uri.Scheme is not ("http" or "https") || uri.UserInfo.Length > 0 || !ValidHost(uri.IdnHost))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
url = uri;
|
||||
host = uri.IdnHost.Trim('[', ']');
|
||||
port = uri.IsDefaultPort ? 0 : uri.Port;
|
||||
return true;
|
||||
}
|
||||
|
||||
// IPAddress.TryParse 会把「[v6]:端口」的端口丢掉,带方括号的先按端点解析
|
||||
if (!target.StartsWith('[') && IPAddress.TryParse(target, out IPAddress? ip))
|
||||
{
|
||||
host = ip.ToString();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (IPEndPoint.TryParse(target, out IPEndPoint? endpoint))
|
||||
{
|
||||
if (endpoint.Port is 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
host = endpoint.Address.ToString();
|
||||
port = endpoint.Port;
|
||||
return true;
|
||||
}
|
||||
|
||||
int colon = target.LastIndexOf(':');
|
||||
string name = colon < 0 ? target : target[..colon];
|
||||
if (colon >= 0 && (!int.TryParse(target.AsSpan(colon + 1), NumberStyles.None, CultureInfo.InvariantCulture, out port) || port is < 1 or > 65535))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ValidHost(name))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
host = name.ToLowerInvariant();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>按 Kind 检查并规范化:<c>http</c> 必须是 http(s) URL,其余 Kind 必须是主机[:端口];不合格返回 null。</summary>
|
||||
public static string? Normalize(string kindId, string? target)
|
||||
{
|
||||
if (!TryParse(target, out string host, out int port, out Uri? url) || (url is not null) != (kindId == HttpKind))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (url is not null)
|
||||
{
|
||||
return url.AbsoluteUri;
|
||||
}
|
||||
|
||||
string shown = IPAddress.TryParse(host, out IPAddress? ip) && ip.AddressFamily == System.Net.Sockets.AddressFamily.InterNetworkV6 && port != 0 ? "[" + host + "]" : host;
|
||||
return port == 0 ? host : shown + ":" + port.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
private static bool ValidHost(string name)
|
||||
{
|
||||
if (IPAddress.TryParse(name.Trim('[', ']'), out _))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (name.Length is 0 or > 253 || name[^1] == '-' || name[0] is '-' or '.')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return name.TrimEnd('.').Split('.').All(label => label.Length is > 0 and <= 63 && label.All(c => char.IsAsciiLetterOrDigit(c) || c == '-'));
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ public enum MeasurementDatagramType : byte
|
||||
EchoRequest = 1,
|
||||
EchoReply = 2,
|
||||
|
||||
/// <summary>单向数据(udp_train),以及 拨号方 为了保持 NAT 映射和让 Agent 学到源地址而发的探活包。</summary>
|
||||
/// <summary>单向数据(quic_stream),以及 拨号方 为了保持 NAT 映射和让 Agent 学到源地址而发的探活包。</summary>
|
||||
Data = 3,
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user