diff --git a/backend/agent/src/MoShiro.Agent/AgentConfig.cs b/backend/agent/src/MoShiro.Agent/AgentConfig.cs
index 6754f55..7b3f140 100644
--- a/backend/agent/src/MoShiro.Agent/AgentConfig.cs
+++ b/backend/agent/src/MoShiro.Agent/AgentConfig.cs
@@ -98,7 +98,7 @@ public sealed class LimitsSection
[JsonPropertyName("max_total_pps")]
public long MaxTotalPps { get; set; } = 20_000;
- /// 默认为空:不允许执行任何目标任务。
+ /// 单个 Agent 任务(外部目标)能探测的网段。默认为空:只允许公网地址,内网、回环、链路本地等一律拒绝;写了就只允许列出的网段(可以包含内网)。
[JsonPropertyName("allowed_target_cidrs")]
public List AllowedTargetCidrs { get; set; } = [];
diff --git a/backend/shared/MoShiro.Measurement/Execution/BudgetGuard.cs b/backend/shared/MoShiro.Measurement/Execution/BudgetGuard.cs
index f589594..061edda 100644
--- a/backend/shared/MoShiro.Measurement/Execution/BudgetGuard.cs
+++ b/backend/shared/MoShiro.Measurement/Execution/BudgetGuard.cs
@@ -107,8 +107,8 @@ public sealed class BudgetGuard
}
///
- /// 目标在静态上可不可以接受:IP 字面量按 allowed_target_cidrs 检查;域名(包括 URL 里的)要解析之后才知道,
- /// 这里只要求配置过白名单,运行时每次解析出的地址再用 检查。
+ /// 目标在静态上可不可以接受:IP 字面量直接用 判断;域名(包括 URL 里的)要解析之后才知道,
+ /// 这里先接受,运行时每次解析出的地址再逐个检查。
///
public bool TargetAllowed(string? target)
{
@@ -117,9 +117,13 @@ public sealed class BudgetGuard
return false;
}
- return IPAddress.TryParse(host, out IPAddress? address) ? AddressAllowed(address) : _cidrs.Count > 0;
+ return !IPAddress.TryParse(host, out IPAddress? address) || AddressAllowed(address);
}
+ ///
+ /// 这个地址能不能作为单个 Agent 任务的目标。allowed_target_cidrs 没配置时只允许公网地址(内网、回环、链路本地、组播等一律拒绝),
+ /// 防止被攻破的 Backend 借 Agent 探测它所在的内网;配置了就只允许列出的网段(包括内网段,由节点所有者显式决定)。
+ ///
public bool AddressAllowed(IPAddress address)
{
if (address.IsIPv4MappedToIPv6)
@@ -127,6 +131,11 @@ public sealed class BudgetGuard
address = address.MapToIPv4();
}
+ if (_limits.AllowedTargetCidrs.Count == 0)
+ {
+ return IsPublic(address);
+ }
+
foreach ((IPAddress network, int prefix) in _cidrs)
{
if (InCidr(address, network, prefix))
@@ -138,6 +147,28 @@ public sealed class BudgetGuard
return false;
}
+ private static readonly (IPAddress Network, int Prefix)[] s_nonPublic =
+ [
+ (IPAddress.Parse("0.0.0.0"), 8), (IPAddress.Parse("10.0.0.0"), 8), (IPAddress.Parse("100.64.0.0"), 10), (IPAddress.Parse("127.0.0.0"), 8),
+ (IPAddress.Parse("169.254.0.0"), 16), (IPAddress.Parse("172.16.0.0"), 12), (IPAddress.Parse("192.0.0.0"), 24), (IPAddress.Parse("192.168.0.0"), 16),
+ (IPAddress.Parse("198.18.0.0"), 15), (IPAddress.Parse("224.0.0.0"), 3),
+ (IPAddress.Parse("::"), 128), (IPAddress.Parse("::1"), 128), (IPAddress.Parse("fc00::"), 7), (IPAddress.Parse("fe80::"), 10), (IPAddress.Parse("ff00::"), 8),
+ ];
+
+ /// 不是公网:私有、回环、链路本地、运营商级 NAT、组播、保留地址。
+ private static bool IsPublic(IPAddress address)
+ {
+ foreach ((IPAddress network, int prefix) in s_nonPublic)
+ {
+ if (InCidr(address, network, prefix))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
private bool PortAllowed(KindSpec kind, ParamValues values)
{
if (!kind.Params.Any(p => p.Name == "port"))
diff --git a/backend/shared/tests/MoShiro.Measurement.Tests/MeasurementTests.cs b/backend/shared/tests/MoShiro.Measurement.Tests/MeasurementTests.cs
index fe5d99a..633d327 100644
--- a/backend/shared/tests/MoShiro.Measurement.Tests/MeasurementTests.cs
+++ b/backend/shared/tests/MoShiro.Measurement.Tests/MeasurementTests.cs
@@ -46,24 +46,37 @@ public sealed class MeasurementTests
}
[Fact]
- public void BudgetGuard_TargetTasks_AreDeniedUnlessTheAddressIsInAnAllowedRange()
+ public void BudgetGuard_TargetTasks_DefaultToPublicAddressesOnly_AndAnExplicitListIsExactly_WhatItSays()
{
- Assert.False(Guard().TargetAllowed("1.1.1.1"));
+ // 没配置白名单:公网地址允许,内网、回环、链路本地、运营商 NAT、组播一律拒绝;域名要解析后逐个地址检查
+ BudgetGuard open = Guard();
+ foreach (string allowed in new[] { "1.1.1.1", "1.1.1.1:443", "2606:4700::1111", "example.com:443", "https://example.com/health", "::ffff:8.8.8.8" })
+ {
+ Assert.True(open.TargetAllowed(allowed), allowed);
+ }
- BudgetGuard some = Guard(new NodeLimits { AllowedTargetCidrs = ["192.0.2.0/24", "2001:db8::/32"] });
- Assert.True(some.TargetAllowed("192.0.2.17"));
+ foreach (string denied in new[] { "10.1.2.3", "172.16.0.1", "172.31.255.255", "192.168.1.1:80", "127.0.0.1", "169.254.169.254", "100.64.0.1", "224.0.0.1", "0.0.0.0", "::1", "fe80::1", "fd00::1", "::ffff:10.0.0.1", "http://192.168.1.1/" })
+ {
+ Assert.False(open.TargetAllowed(denied), denied);
+ }
+
+ Assert.False(open.TargetAllowed(null));
+ Assert.True(open.AddressAllowed(IPAddress.Parse("172.32.0.1"))); // 172.16/12 的外面是公网
+
+ // 配置了白名单:只允许列出的网段,可以包含内网,其余(包括公网)都不允许
+ BudgetGuard some = Guard(new NodeLimits { AllowedTargetCidrs = ["192.0.2.0/24", "2001:db8::/32", "10.0.0.0/8"] });
Assert.True(some.TargetAllowed("192.0.2.17:443"));
Assert.True(some.TargetAllowed("2001:db8::1"));
+ Assert.True(some.TargetAllowed("10.9.9.9"));
+ Assert.False(some.TargetAllowed("1.1.1.1"));
Assert.False(some.TargetAllowed("192.0.3.1"));
- Assert.False(some.TargetAllowed(null));
-
- // 域名要解析之后才知道地址:配置过白名单就先接受,每个解析出的地址再由 AddressAllowed 检查
- Assert.True(some.TargetAllowed("example.com:443"));
- Assert.True(some.TargetAllowed("https://example.com/health"));
- Assert.False(Guard().TargetAllowed("example.com"));
+ Assert.True(some.TargetAllowed("example.com"));
Assert.True(some.AddressAllowed(IPAddress.Parse("192.0.2.9")));
- Assert.False(some.AddressAllowed(IPAddress.Parse("10.0.0.1")));
- Assert.False(some.AddressAllowed(IPAddress.Parse("::ffff:10.0.0.1")));
+ Assert.False(some.AddressAllowed(IPAddress.Parse("8.8.8.8")));
+ Assert.False(some.AddressAllowed(IPAddress.Parse("::ffff:8.8.8.8")));
+
+ // 写了但一条也解析不出来:当成「什么都不允许」,不退回到默认
+ Assert.False(Guard(new NodeLimits { AllowedTargetCidrs = ["not-a-network"] }).AddressAllowed(IPAddress.Parse("1.1.1.1")));
}
// ---- RowStore ----