Skip to content

Commit 96812bd

Browse files
committed
align dynet probe protocols with manifests
1 parent be1a6ee commit 96812bd

15 files changed

Lines changed: 295 additions & 69 deletions

File tree

‎crates/dynet-cli/src/cli/help.rs‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Commands:
1818
[--log-level off|error|warn|info|debug|trace]
1919
probe [--root <path>] [--config <path>] [--format text|json]
2020
--url https://host/path | --host <host> [--port 443] [--path /]
21+
[--protocol https-head|tls-handshake]
2122
[--inbound <tag>] [--quality-state <path>]
2223
[--log-level off|error|warn|info|debug|trace]
2324
repair [--format text|json]
@@ -47,8 +48,8 @@ Reports:
4748
install --check reports network ownership preflight, owned-resource scope,
4849
render-only desired state artifacts, and artifact validation status.
4950
plan compiles explicit routes into an explainable plan model.
50-
probe runs an explicit dynet-observed HTTPS HEAD through route, plan,
51-
outbound selection, and outbound stage tracing. probe is unprivileged by
51+
probe runs an explicit dynet-observed HTTPS HEAD or TLS handshake through
52+
route, plan, outbound selection, and outbound stage tracing. probe is unprivileged by
5253
default and does not apply the runtime socket mark. --quality-state loads an
5354
expiring observation snapshot for quality-aware diagnostic strategies.
5455
status, verify, repair, and uninstall report dynet-owned resource state.

‎crates/dynet-cli/src/cli/mod.rs‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ mod values;
66

77
pub(crate) use help::help_text;
88
pub(crate) use types::*;
9-
use values::{parse_format, parse_log_level, parse_u16, parse_u32, parse_u64, parse_usize};
9+
use values::{
10+
parse_format, parse_log_level, parse_probe_protocol, parse_u16, parse_u32, parse_u64,
11+
parse_usize,
12+
};
1013

1114
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
1215
enum CommandMode {
@@ -56,6 +59,7 @@ pub(crate) fn parse_args(args: Vec<String>) -> Result<CliCommand, String> {
5659
let mut probe_path = None;
5760
let mut probe_inbound = None;
5861
let mut probe_quality_state = None;
62+
let mut probe_protocol = ProbeProtocol::HttpsHead;
5963
let mut run_max_dns_queries = None;
6064
let mut run_max_tun_packets = None;
6165
let mut run_max_tcp_sessions = None;
@@ -221,6 +225,12 @@ pub(crate) fn parse_args(args: Vec<String>) -> Result<CliCommand, String> {
221225
"--quality-state requires a JSON path".to_string()
222226
})?));
223227
}
228+
"--protocol" if mode == CommandMode::Probe => {
229+
let value = args
230+
.next()
231+
.ok_or_else(|| "--protocol requires https-head or tls-handshake".to_string())?;
232+
probe_protocol = parse_probe_protocol(&value)?;
233+
}
224234
"--max-dns-queries" if mode == CommandMode::Run => {
225235
let value = args
226236
.next()
@@ -314,6 +324,9 @@ pub(crate) fn parse_args(args: Vec<String>) -> Result<CliCommand, String> {
314324
other if mode == CommandMode::Probe && other.starts_with("--quality-state=") => {
315325
probe_quality_state = Some(PathBuf::from(&other["--quality-state=".len()..]));
316326
}
327+
other if mode == CommandMode::Probe && other.starts_with("--protocol=") => {
328+
probe_protocol = parse_probe_protocol(&other["--protocol=".len()..])?;
329+
}
317330
other if mode == CommandMode::Run && other.starts_with("--max-dns-queries=") => {
318331
run_max_dns_queries = Some(parse_usize(
319332
"--max-dns-queries",
@@ -387,6 +400,7 @@ pub(crate) fn parse_args(args: Vec<String>) -> Result<CliCommand, String> {
387400
}),
388401
CommandMode::Probe => CliCommand::Probe(ProbeOptions {
389402
command: options,
403+
protocol: probe_protocol,
390404
url: probe_url,
391405
host: probe_host,
392406
port: probe_port,

‎crates/dynet-cli/src/cli/types.rs‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ pub(crate) struct RunOptions {
6767
#[derive(Debug, Clone, Eq, PartialEq)]
6868
pub(crate) struct ProbeOptions {
6969
pub(crate) command: CommandOptions,
70+
pub(crate) protocol: ProbeProtocol,
7071
pub(crate) url: Option<String>,
7172
pub(crate) host: Option<String>,
7273
pub(crate) port: Option<u16>,
@@ -75,6 +76,12 @@ pub(crate) struct ProbeOptions {
7576
pub(crate) quality_state: Option<PathBuf>,
7677
}
7778

79+
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
80+
pub(crate) enum ProbeProtocol {
81+
HttpsHead,
82+
TlsHandshake,
83+
}
84+
7885
#[derive(Debug, Clone, Eq, PartialEq)]
7986
pub(crate) struct InstallOptions {
8087
pub(crate) lifecycle: LifecycleOptions,

‎crates/dynet-cli/src/cli/values.rs‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use super::{LogLevel, OutputFormat};
1+
use super::{LogLevel, OutputFormat, ProbeProtocol};
22

33
pub(super) fn parse_format(value: &str) -> Result<OutputFormat, String> {
44
match value {
@@ -20,6 +20,14 @@ pub(super) fn parse_log_level(value: &str) -> Result<LogLevel, String> {
2020
}
2121
}
2222

23+
pub(super) fn parse_probe_protocol(value: &str) -> Result<ProbeProtocol, String> {
24+
match value {
25+
"https-head" => Ok(ProbeProtocol::HttpsHead),
26+
"tls-handshake" => Ok(ProbeProtocol::TlsHandshake),
27+
other => Err(format!("unsupported probe protocol: {other}")),
28+
}
29+
}
30+
2331
pub(super) fn parse_u64(flag: &str, value: &str) -> Result<u64, String> {
2432
value
2533
.parse()

‎crates/dynet-cli/src/main.rs‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,12 +195,16 @@ fn run_probe_command(options: cli::ProbeOptions) -> Result<i32, String> {
195195
} else {
196196
dynet_runtime::RuntimePolicy::from_config(resolved.config)
197197
};
198-
let report = dynet_runtime::probe_https_head(dynet_runtime::ProbeSettings {
198+
let settings = dynet_runtime::ProbeSettings {
199199
target,
200200
inbound: options.inbound,
201201
bypass_mark,
202202
policy,
203-
})?;
203+
};
204+
let report = match options.protocol {
205+
cli::ProbeProtocol::HttpsHead => dynet_runtime::probe_https_head(settings),
206+
cli::ProbeProtocol::TlsHandshake => dynet_runtime::probe_tls_handshake(settings),
207+
}?;
204208
print_probe_report(&report, options.command.format)?;
205209
Ok(if report.status == dynet_runtime::RuntimeStatus::Pass {
206210
0

‎crates/dynet-cli/src/output/runtime.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ pub(crate) fn text_probe_report(report: &dynet_runtime::ProbeReport) -> String {
6262
};
6363
writeln!(&mut text, "dynet probe {status}: {}", report.reason).expect("write string");
6464
writeln!(&mut text, "probe model: {}", report.schema).expect("write string");
65+
writeln!(&mut text, "protocol: {}", report.protocol.as_str()).expect("write string");
6566
writeln!(
6667
&mut text,
6768
"target: https://{}:{}{}",

‎crates/dynet-cli/tests/unit/cli.rs‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use std::path::PathBuf;
22

33
use crate::cli::{
44
help_text, parse_args, ApiCommand, ApiOptions, ApiServeOptions, CliCommand, InstallOptions,
5-
LifecycleOptions, LogLevel, OutputFormat,
5+
LifecycleOptions, LogLevel, OutputFormat, ProbeProtocol,
66
};
77

88
#[test]
@@ -126,6 +126,7 @@ fn parses_probe_options() {
126126
"--inbound=tun-in".into(),
127127
"--quality-state".into(),
128128
".task/resources/quality.json".into(),
129+
"--protocol=tls-handshake".into(),
129130
"--format=json".into(),
130131
])
131132
.unwrap() else {
@@ -134,6 +135,7 @@ fn parses_probe_options() {
134135

135136
assert_eq!(options.command.config, Some(PathBuf::from("proxy.json")));
136137
assert_eq!(options.command.format, OutputFormat::Json);
138+
assert_eq!(options.protocol, ProbeProtocol::TlsHandshake);
137139
assert_eq!(options.url.as_deref(), Some("https://chatgpt.com/"));
138140
assert_eq!(options.inbound.as_deref(), Some("tun-in"));
139141
assert_eq!(

‎crates/dynet-cli/tests/unit/output.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,7 @@ fn probe_report_is_rendered() {
355355
schema: "dynet-probe/v1alpha1".to_string(),
356356
status: dynet_runtime::RuntimeStatus::Pass,
357357
reason: "HTTPS HEAD completed with HTTP 200".to_string(),
358+
protocol: dynet_runtime::ProbeProtocol::HttpsHead,
358359
target: dynet_runtime::ProbeTarget {
359360
host: "example.com".to_string(),
360361
port: 443,
@@ -376,6 +377,7 @@ fn probe_report_is_rendered() {
376377

377378
assert!(text.contains("dynet probe passed"));
378379
assert!(text.contains("probe model: dynet-probe/v1alpha1"));
380+
assert!(text.contains("protocol: https-head"));
379381
assert!(text.contains("target: https://example.com:443/"));
380382
assert!(text.contains("runtime events:"));
381383
print_probe_report(&report, OutputFormat::Json).unwrap();

‎crates/dynet-runtime/src/lib.rs‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,9 @@ use serde::Serialize;
141141

142142
pub use dns::dns_reverse_from_wire;
143143
pub use event::{RuntimeEvent, RuntimeEventKind};
144-
pub use probe::{probe_https_head, ProbeReport, ProbeSettings, ProbeTarget};
144+
pub use probe::{
145+
probe_https_head, probe_tls_handshake, ProbeProtocol, ProbeReport, ProbeSettings, ProbeTarget,
146+
};
145147
pub use settings::{
146148
DnsRuntimeChain, RunLimits, RuntimePolicy, RuntimeSettings, TakeoverSettings,
147149
TcpForwardingSettings, UdpForwardingSettings,

‎crates/dynet-runtime/src/probe/http.rs‎

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,30 +11,37 @@ use rustls::{ClientConfig, ClientConnection, RootCertStore, StreamOwned};
1111
use crate::{
1212
event::EventBus,
1313
outbound::ProxiedTcpStream,
14-
probe::ProbeTarget,
14+
probe::{ProbeProtocol, ProbeTarget},
1515
resolver::trace::{classify_runtime_error, elapsed_ms},
1616
RuntimeEvent, RuntimeEventKind,
1717
};
1818

1919
const HTTP_HEAD_LIMIT: usize = 64 * 1024;
2020

21-
pub(crate) struct HttpHeadResponse {
22-
pub(crate) status_code: u16,
21+
pub(crate) struct ProbeResponse {
22+
pub(crate) status_code: Option<u16>,
2323
pub(crate) bytes: usize,
2424
}
2525

26-
pub(crate) fn execute(
26+
pub(crate) fn execute_with_protocol(
2727
ebus: &EventBus,
2828
outbound: &NetworkNode,
2929
target: &ProbeTarget,
3030
stream: ProxiedTcpStream,
31-
) -> Result<HttpHeadResponse, String> {
31+
protocol: ProbeProtocol,
32+
) -> Result<ProbeResponse, String> {
3233
let mut tls = observe_stage(ebus, outbound, "tls-handshake", || {
3334
tls_handshake(
34-
ObservedProbeStream::new(stream, ebus.clone(), outbound, "https-head"),
35+
ObservedProbeStream::new(stream, ebus.clone(), outbound, protocol.as_str()),
3536
&target.host,
3637
)
3738
})?;
39+
if protocol == ProbeProtocol::TlsHandshake {
40+
return Ok(ProbeResponse {
41+
status_code: None,
42+
bytes: 0,
43+
});
44+
}
3845
observe_stage(ebus, outbound, "http-head-write", || {
3946
let request = http_head_request(target);
4047
tls.write_all(&request)
@@ -131,7 +138,7 @@ fn http_head_request(target: &ProbeTarget) -> Vec<u8> {
131138

132139
fn read_http_head(
133140
tls: &mut StreamOwned<ClientConnection, ObservedProbeStream>,
134-
) -> Result<HttpHeadResponse, String> {
141+
) -> Result<ProbeResponse, String> {
135142
let mut response = Vec::new();
136143
let mut buffer = [0_u8; 4096];
137144
loop {
@@ -166,8 +173,8 @@ fn read_http_head(
166173
.ok_or_else(|| format!("HTTPS HEAD status line has no code: `{status_line}`"))?
167174
.parse::<u16>()
168175
.map_err(|error| format!("invalid HTTPS HEAD status line `{status_line}`: {error}"))?;
169-
Ok(HttpHeadResponse {
170-
status_code,
176+
Ok(ProbeResponse {
177+
status_code: Some(status_code),
171178
bytes: response.len(),
172179
})
173180
}

0 commit comments

Comments
 (0)