mirror of
https://github.com/Horizontal42/netsleuth.git
synced 2026-11-10 15:42:10 +03:00
add config sections for TLS, prefix bench, DPI check, DNS advanced, path diversity
This commit is contained in:
parent
33d7d2d942
commit
c63c179b57
3 changed files with 212 additions and 0 deletions
47
config.yaml
47
config.yaml
|
|
@ -67,6 +67,53 @@ thresholds:
|
|||
loss_pct: { good: 0.0, warn: 2.0 }
|
||||
bufferbloat_ms: { a: 5.0, b: 30.0, c: 60.0, d: 200.0, e: 400.0 }
|
||||
vpn_confidence: { likely: 0.40, confirmed: 0.75 }
|
||||
tls_handshake_ms: { good: 100.0, warn: 300.0 }
|
||||
ttfb_ms: { good: 200.0, warn: 800.0 }
|
||||
dns_resolve_ms: { good: 30.0, warn: 120.0 }
|
||||
prefix_spread_ms: { good: 20.0, warn: 80.0 }
|
||||
tls_cpu_bound_ratio: 2.0
|
||||
|
||||
tls:
|
||||
targets:
|
||||
- { label: "cloudflare", host: "cloudflare.com" }
|
||||
- { label: "google", host: "google.com" }
|
||||
port: 443
|
||||
concurrency: 4
|
||||
ttfb_path: "/"
|
||||
|
||||
# Opt-in via --prefix-bench. Pings the first host of a handful of your own AS's
|
||||
# announced prefixes to find the lowest-latency PoP. max_prefixes is capped at 256
|
||||
# in code: this benchmarks a sample of your own network, not a mass scan of it.
|
||||
prefix_bench:
|
||||
max_prefixes: 32
|
||||
ping_count: 3
|
||||
concurrency: 8
|
||||
host_offset: 1
|
||||
family: 4
|
||||
|
||||
# Opt-in via --my-server <host>. Single-host self-diagnostic for provider port
|
||||
# blocking/TCP RST injection. ports/concurrency are capped in code so this cannot
|
||||
# become a range scanner: never point it at infrastructure you don't own.
|
||||
dpi_check:
|
||||
ports: [80, 443, 8443, 2083, 2096, 53]
|
||||
connect_timeout_seconds: 3.0
|
||||
delay_between_ports_seconds: 0.25
|
||||
concurrency: 2
|
||||
|
||||
dns_advanced:
|
||||
control_names: ["cloudflare.com", "example.com"]
|
||||
doh_endpoints:
|
||||
- { name: "cloudflare", url: "https://cloudflare-dns.com/dns-query" }
|
||||
- { name: "google", url: "https://dns.google/resolve" }
|
||||
- { name: "quad9", url: "https://dns.quad9.net:5053/dns-query" }
|
||||
bogus_resolver_ip: "192.0.2.1"
|
||||
bogus_probe_name: "cloudflare.com"
|
||||
|
||||
path_diversity:
|
||||
targets:
|
||||
- { label: "cloudflare", host: "cloudflare.com" }
|
||||
- { label: "discord", host: "discord.com" }
|
||||
max_targets: 3
|
||||
|
||||
output:
|
||||
logs_dir: "./logs"
|
||||
|
|
|
|||
|
|
@ -120,6 +120,93 @@ class Thresholds(BaseModel):
|
|||
loss_pct: Band = Band(good=0.0, warn=2.0)
|
||||
bufferbloat_ms: BufferbloatBands = BufferbloatBands()
|
||||
vpn_confidence: VpnBands = VpnBands()
|
||||
tls_handshake_ms: Band = Band(good=100.0, warn=300.0)
|
||||
ttfb_ms: Band = Band(good=200.0, warn=800.0)
|
||||
dns_resolve_ms: Band = Band(good=30.0, warn=120.0)
|
||||
prefix_spread_ms: Band = Band(good=20.0, warn=80.0)
|
||||
tls_cpu_bound_ratio: float = 2.0
|
||||
|
||||
|
||||
class TlsConfig(BaseModel):
|
||||
targets: list[HostSpec] = Field(
|
||||
default_factory=lambda: [
|
||||
HostSpec(label="cloudflare", host="cloudflare.com"),
|
||||
HostSpec(label="google", host="google.com"),
|
||||
]
|
||||
)
|
||||
port: int = 443
|
||||
concurrency: int = 4
|
||||
ttfb_path: str = "/"
|
||||
|
||||
|
||||
class PrefixBenchConfig(BaseModel):
|
||||
max_prefixes: int = 32
|
||||
ping_count: int = 3
|
||||
concurrency: int = 8
|
||||
host_offset: int = 1
|
||||
family: int = 4
|
||||
|
||||
@field_validator("max_prefixes")
|
||||
@classmethod
|
||||
def _cap_max_prefixes(cls, value: int) -> int:
|
||||
if value > 256:
|
||||
raise ValueError(
|
||||
"prefix_bench.max_prefixes may not exceed 256 "
|
||||
"(this benchmarks a handful of your own AS's PoPs, not a mass scan of the whole AS)"
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
class DpiCheckConfig(BaseModel):
|
||||
ports: list[int] = Field(default_factory=lambda: [80, 443, 8443, 2083, 2096, 53])
|
||||
connect_timeout_seconds: float = 3.0
|
||||
delay_between_ports_seconds: float = 0.25
|
||||
concurrency: int = 2
|
||||
|
||||
@field_validator("ports")
|
||||
@classmethod
|
||||
def _cap_ports(cls, value: list[int]) -> list[int]:
|
||||
if len(value) > 16:
|
||||
raise ValueError(
|
||||
"dpi_check.ports may list at most 16 ports "
|
||||
"(this is a single-host self-diagnostic, not a port-range scanner)"
|
||||
)
|
||||
return value
|
||||
|
||||
@field_validator("concurrency")
|
||||
@classmethod
|
||||
def _cap_concurrency(cls, value: int) -> int:
|
||||
if value > 4:
|
||||
raise ValueError("dpi_check.concurrency may not exceed 4 (keep the self-check rate-limited)")
|
||||
return value
|
||||
|
||||
|
||||
class DohEndpoint(BaseModel):
|
||||
name: str
|
||||
url: str
|
||||
|
||||
|
||||
class DnsAdvancedConfig(BaseModel):
|
||||
control_names: list[str] = Field(default_factory=lambda: ["cloudflare.com", "example.com"])
|
||||
doh_endpoints: list[DohEndpoint] = Field(
|
||||
default_factory=lambda: [
|
||||
DohEndpoint(name="cloudflare", url="https://cloudflare-dns.com/dns-query"),
|
||||
DohEndpoint(name="google", url="https://dns.google/resolve"),
|
||||
DohEndpoint(name="quad9", url="https://dns.quad9.net:5053/dns-query"),
|
||||
]
|
||||
)
|
||||
bogus_resolver_ip: str = "192.0.2.1"
|
||||
bogus_probe_name: str = "cloudflare.com"
|
||||
|
||||
|
||||
class PathDiversityConfig(BaseModel):
|
||||
targets: list[HostSpec] = Field(
|
||||
default_factory=lambda: [
|
||||
HostSpec(label="cloudflare", host="cloudflare.com"),
|
||||
HostSpec(label="discord", host="discord.com"),
|
||||
]
|
||||
)
|
||||
max_targets: int = 3
|
||||
|
||||
|
||||
class Output(BaseModel):
|
||||
|
|
@ -169,6 +256,11 @@ class Settings(BaseSettings):
|
|||
thresholds: Thresholds = Thresholds()
|
||||
output: Output = Output()
|
||||
watch: Watch = Watch()
|
||||
tls: TlsConfig = TlsConfig()
|
||||
prefix_bench: PrefixBenchConfig = PrefixBenchConfig()
|
||||
dpi_check: DpiCheckConfig = DpiCheckConfig()
|
||||
dns_advanced: DnsAdvancedConfig = DnsAdvancedConfig()
|
||||
path_diversity: PathDiversityConfig = PathDiversityConfig()
|
||||
|
||||
ipinfo_token: SecretStr | None = Field(default=None, alias="IPINFO_TOKEN")
|
||||
peeringdb_api_key: SecretStr | None = Field(default=None, alias="PEERINGDB_API_KEY")
|
||||
|
|
|
|||
|
|
@ -7,6 +7,24 @@ from pydantic import ValidationError
|
|||
|
||||
from netsleuth.config import load_settings
|
||||
|
||||
L7_YAML = textwrap.dedent(
|
||||
"""
|
||||
tls:
|
||||
port: 8443
|
||||
concurrency: 2
|
||||
prefix_bench:
|
||||
max_prefixes: 8
|
||||
dpi_check:
|
||||
delay_between_ports_seconds: 0.5
|
||||
dns_advanced:
|
||||
bogus_resolver_ip: "198.51.100.1"
|
||||
path_diversity:
|
||||
max_targets: 5
|
||||
thresholds:
|
||||
tls_handshake_ms: { good: 50.0, warn: 150.0 }
|
||||
"""
|
||||
).strip()
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def yaml_file(tmp_path):
|
||||
|
|
@ -123,3 +141,58 @@ def test_output_formats_rejects_unknown_value(tmp_path):
|
|||
path.write_text('output:\n formats: ["pdf"]\n', encoding="utf-8")
|
||||
with pytest.raises(ValidationError):
|
||||
load_settings(config_path=path, env_file=tmp_path / "nope.env")
|
||||
|
||||
|
||||
def test_l7_sections_default_without_any_yaml(tmp_path):
|
||||
s = load_settings(config_path=tmp_path / "nope.yaml", env_file=tmp_path / "nope.env")
|
||||
assert s.tls.port == 443
|
||||
assert s.tls.targets
|
||||
assert s.prefix_bench.max_prefixes == 32
|
||||
assert s.dpi_check.ports == [80, 443, 8443, 2083, 2096, 53]
|
||||
assert s.dns_advanced.doh_endpoints
|
||||
assert s.path_diversity.targets
|
||||
assert s.thresholds.tls_handshake_ms.warn == 300.0
|
||||
assert s.thresholds.tls_cpu_bound_ratio == 2.0
|
||||
|
||||
|
||||
def test_l7_sections_yaml_overrides(tmp_path):
|
||||
path = tmp_path / "config.yaml"
|
||||
path.write_text(L7_YAML, encoding="utf-8")
|
||||
s = load_settings(config_path=path, env_file=tmp_path / "nope.env")
|
||||
assert s.tls.port == 8443
|
||||
assert s.tls.concurrency == 2
|
||||
assert s.prefix_bench.max_prefixes == 8
|
||||
assert s.dpi_check.delay_between_ports_seconds == 0.5
|
||||
assert s.dns_advanced.bogus_resolver_ip == "198.51.100.1"
|
||||
assert s.path_diversity.max_targets == 5
|
||||
assert s.thresholds.tls_handshake_ms.good == 50.0
|
||||
|
||||
|
||||
def test_env_overrides_an_l7_section_field(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("NETSLEUTH_PREFIX_BENCH__MAX_PREFIXES", "16")
|
||||
s = load_settings(config_path=tmp_path / "nope.yaml", env_file=tmp_path / "nope.env")
|
||||
assert s.prefix_bench.max_prefixes == 16
|
||||
|
||||
|
||||
def test_dpi_check_ports_are_capped_so_it_cannot_become_a_range_scanner(tmp_path):
|
||||
path = tmp_path / "config.yaml"
|
||||
path.write_text(
|
||||
"dpi_check:\n ports: [" + ", ".join(str(80 + i) for i in range(17)) + "]\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
with pytest.raises(ValidationError, match="at most 16 ports"):
|
||||
load_settings(config_path=path, env_file=tmp_path / "nope.env")
|
||||
|
||||
|
||||
def test_dpi_check_concurrency_is_capped_as_a_rate_limit(tmp_path):
|
||||
path = tmp_path / "config.yaml"
|
||||
path.write_text("dpi_check:\n concurrency: 10\n", encoding="utf-8")
|
||||
with pytest.raises(ValidationError, match="concurrency"):
|
||||
load_settings(config_path=path, env_file=tmp_path / "nope.env")
|
||||
|
||||
|
||||
def test_prefix_bench_max_prefixes_is_capped_so_it_cannot_scan_a_whole_as(tmp_path):
|
||||
path = tmp_path / "config.yaml"
|
||||
path.write_text("prefix_bench:\n max_prefixes: 1000\n", encoding="utf-8")
|
||||
with pytest.raises(ValidationError, match="256"):
|
||||
load_settings(config_path=path, env_file=tmp_path / "nope.env")
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue