1use std::io::ErrorKind;
7
8use zebra_chain::{
9 parameters::{Network, NetworkKind},
10 serialization::SerializationError,
11};
12
13use crate::{
14 peer::{ConnectedAddr, HandshakeError},
15 BoxError, PeerSocketAddr,
16};
17
18const CONNECTION_ATTEMPTS_TOTAL: &str = "zcash.net.connection.attempts.total";
19const CONNECTION_OUTCOMES_TOTAL: &str = "zcash.net.connection.outcomes.total";
20const REMOTE_VERSION_MESSAGES_TOTAL: &str = "zcash.net.peer.version.messages.total";
21const REMOTE_VERSION_OUTCOMES_TOTAL: &str = "zcash.net.peer.version.outcomes.total";
22
23#[derive(Copy, Clone, Debug, Eq, PartialEq)]
24pub(crate) enum ConnectionDirection {
25 Inbound,
26 Outbound,
27}
28
29impl ConnectionDirection {
30 fn label(self) -> &'static str {
31 match self {
32 Self::Inbound => "inbound",
33 Self::Outbound => "outbound",
34 }
35 }
36}
37
38#[derive(Copy, Clone, Debug, Eq, PartialEq)]
39struct ConnectionErrorLabels {
40 stage: &'static str,
41 outcome: &'static str,
42}
43
44#[derive(Copy, Clone, Debug, Eq, PartialEq)]
45struct RemoteVersionMetricLabels {
46 network: &'static str,
47 direction: &'static str,
48 address_family: &'static str,
49 implementation: &'static str,
50}
51
52#[must_use = "dropping the guard records a canceled remote Version outcome"]
57pub(crate) struct RemoteVersionOutcomeGuard {
58 labels: Option<RemoteVersionMetricLabels>,
59 outcome_recorded: bool,
60}
61
62impl RemoteVersionOutcomeGuard {
63 pub(crate) fn new(network: &Network, connected_addr: &ConnectedAddr, user_agent: &str) -> Self {
65 let labels =
66 connection_labels(connected_addr).map(|(direction, addr)| RemoteVersionMetricLabels {
67 network: network_kind_label(network),
68 direction: direction.label(),
69 address_family: address_family_label(addr),
70 implementation: implementation_label(user_agent),
71 });
72
73 if let Some(labels) = labels {
74 metrics::counter!(
75 REMOTE_VERSION_MESSAGES_TOTAL,
76 "network" => labels.network,
77 "direction" => labels.direction,
78 "address_family" => labels.address_family,
79 "implementation" => labels.implementation,
80 )
81 .increment(1);
82 }
83
84 Self {
85 labels,
86 outcome_recorded: false,
87 }
88 }
89
90 pub(crate) fn record_error(&mut self, error: HandshakeError) -> HandshakeError {
92 self.record_outcome(handshake_error_outcome(&error));
93 error
94 }
95
96 pub(crate) fn record_success(mut self) {
98 self.record_outcome("success");
99 }
100
101 fn record_outcome(&mut self, outcome: &'static str) {
102 if std::mem::replace(&mut self.outcome_recorded, true) {
103 return;
104 }
105
106 let Some(labels) = self.labels else {
107 return;
108 };
109
110 metrics::counter!(
111 REMOTE_VERSION_OUTCOMES_TOTAL,
112 "network" => labels.network,
113 "direction" => labels.direction,
114 "address_family" => labels.address_family,
115 "implementation" => labels.implementation,
116 "outcome" => outcome,
117 )
118 .increment(1);
119 }
120}
121
122impl Drop for RemoteVersionOutcomeGuard {
123 fn drop(&mut self) {
124 self.record_outcome("canceled");
125 }
126}
127
128pub(crate) fn network_kind_label(network: &Network) -> &'static str {
130 match network.kind() {
131 NetworkKind::Mainnet => "mainnet",
132 NetworkKind::Testnet => "testnet",
133 NetworkKind::Regtest => "regtest",
134 }
135}
136
137pub(crate) fn record_connection_attempt_started(
139 network: &Network,
140 direction: ConnectionDirection,
141 addr: PeerSocketAddr,
142) {
143 metrics::counter!(
144 CONNECTION_ATTEMPTS_TOTAL,
145 "network" => network_kind_label(network),
146 "direction" => direction.label(),
147 "address_family" => address_family_label(addr),
148 )
149 .increment(1);
150}
151
152pub(crate) fn record_connection_attempt_finished(
154 network: &Network,
155 direction: ConnectionDirection,
156 addr: PeerSocketAddr,
157 error: Option<&BoxError>,
158) {
159 let labels = error.map_or(
160 ConnectionErrorLabels {
161 stage: "handshake",
162 outcome: "success",
163 },
164 |error| classify_connection_error(direction, error),
165 );
166
167 metrics::counter!(
168 CONNECTION_OUTCOMES_TOTAL,
169 "network" => network_kind_label(network),
170 "direction" => direction.label(),
171 "address_family" => address_family_label(addr),
172 "stage" => labels.stage,
173 "outcome" => labels.outcome,
174 )
175 .increment(1);
176}
177
178pub(crate) fn record_inbound_connection_rejected(
180 network: &Network,
181 addr: PeerSocketAddr,
182 outcome: &'static str,
183) {
184 metrics::counter!(
185 CONNECTION_OUTCOMES_TOTAL,
186 "network" => network_kind_label(network),
187 "direction" => ConnectionDirection::Inbound.label(),
188 "address_family" => address_family_label(addr),
189 "stage" => "admission",
190 "outcome" => outcome,
191 )
192 .increment(1);
193}
194
195fn classify_connection_error(
196 direction: ConnectionDirection,
197 error: &BoxError,
198) -> ConnectionErrorLabels {
199 if error.is::<tower::timeout::error::Elapsed>() {
200 return ConnectionErrorLabels {
201 stage: match direction {
202 ConnectionDirection::Inbound => "handshake",
203 ConnectionDirection::Outbound => "tcp_or_handshake",
204 },
205 outcome: "timeout",
206 };
207 }
208
209 if error.is::<tokio::time::error::Elapsed>() {
210 return ConnectionErrorLabels {
211 stage: "handshake",
212 outcome: "timeout",
213 };
214 }
215
216 if let Some(io_error) = error.downcast_ref::<std::io::Error>() {
217 return classify_io_error("tcp_connect", io_error);
218 }
219
220 let Some(handshake_error) = error.downcast_ref::<HandshakeError>() else {
221 return ConnectionErrorLabels {
222 stage: "unknown",
223 outcome: "other",
224 };
225 };
226
227 let stage = match handshake_error {
228 HandshakeError::RemoteNonceReuse
229 | HandshakeError::ObsoleteVersion(_)
230 | HandshakeError::MissingRequiredServices { .. } => "version",
231 _ => "handshake",
232 };
233
234 ConnectionErrorLabels {
235 stage,
236 outcome: handshake_error_outcome(handshake_error),
237 }
238}
239
240fn handshake_error_outcome(error: &HandshakeError) -> &'static str {
241 match error {
242 HandshakeError::UnexpectedMessage(_) => "unexpected_message",
243 HandshakeError::RemoteNonceReuse => "self_connection",
244 HandshakeError::LocalDuplicateNonce => "local_nonce_collision",
245 HandshakeError::ConnectionClosed => "connection_closed",
246 HandshakeError::Io(io_error) => io_error_outcome(io_error),
247 HandshakeError::Serialization(SerializationError::Io(io_error)) => {
248 io_error_outcome(io_error)
249 }
250 HandshakeError::Serialization(_) => "protocol_parse_error",
251 HandshakeError::ObsoleteVersion(_) => "obsolete_version",
252 HandshakeError::MissingRequiredServices { .. } => "missing_required_services",
253 HandshakeError::Timeout => "timeout",
254 }
255}
256
257fn classify_io_error(stage: &'static str, error: &std::io::Error) -> ConnectionErrorLabels {
258 ConnectionErrorLabels {
259 stage,
260 outcome: io_error_outcome(error),
261 }
262}
263
264fn io_error_outcome(error: &std::io::Error) -> &'static str {
265 match error.kind() {
266 ErrorKind::ConnectionRefused => "connection_refused",
267 ErrorKind::ConnectionReset => "connection_reset",
268 ErrorKind::ConnectionAborted => "connection_aborted",
269 ErrorKind::NotConnected => "not_connected",
270 ErrorKind::AddrInUse => "address_in_use",
271 ErrorKind::AddrNotAvailable => "address_not_available",
272 ErrorKind::BrokenPipe => "broken_pipe",
273 ErrorKind::TimedOut => "timeout",
274 ErrorKind::UnexpectedEof => "unexpected_eof",
275 ErrorKind::NetworkUnreachable => "network_unreachable",
276 ErrorKind::HostUnreachable => "host_unreachable",
277 _ => "io_error",
278 }
279}
280
281fn connection_labels(
282 connected_addr: &ConnectedAddr,
283) -> Option<(ConnectionDirection, PeerSocketAddr)> {
284 match connected_addr {
285 ConnectedAddr::OutboundDirect { addr } => Some((ConnectionDirection::Outbound, *addr)),
286 ConnectedAddr::InboundDirect { addr } => Some((ConnectionDirection::Inbound, *addr)),
287 ConnectedAddr::OutboundProxy {
288 transient_local_addr,
289 ..
290 } => Some((
291 ConnectionDirection::Outbound,
292 (*transient_local_addr).into(),
293 )),
294 ConnectedAddr::InboundProxy { transient_addr } => {
295 Some((ConnectionDirection::Inbound, (*transient_addr).into()))
296 }
297 ConnectedAddr::Isolated => None,
298 }
299}
300
301fn address_family_label(addr: PeerSocketAddr) -> &'static str {
302 if addr.is_ipv4() {
303 "ipv4"
304 } else {
305 "ipv6"
306 }
307}
308
309fn implementation_label(user_agent: &str) -> &'static str {
310 if user_agent.starts_with("/Zakura:") {
311 "zakura"
312 } else if user_agent.starts_with("/Zebra:") {
313 "zebra"
314 } else if user_agent.starts_with("/MagicBean:") {
315 "legacy_zcashd"
316 } else {
317 "other"
318 }
319}
320
321#[cfg(test)]
322mod tests {
323 use std::{
324 future, io,
325 sync::{Arc, Mutex},
326 time::Duration,
327 };
328
329 use super::*;
330 use metrics::{
331 Counter, Gauge, Histogram, Key, KeyName, Metadata, Recorder, SharedString, Unit,
332 };
333 use tower::{service_fn, ServiceExt};
334
335 #[derive(Default)]
336 struct CounterRegistrationRecorder {
337 keys: Mutex<Vec<(String, Vec<(String, String)>)>>,
338 }
339
340 impl CounterRegistrationRecorder {
341 fn label_values(&self, metric_name: &str, label_name: &str) -> Vec<String> {
342 self.keys
343 .lock()
344 .expect("the test recorder mutex must not be poisoned")
345 .iter()
346 .filter(|(name, _)| name == metric_name)
347 .filter_map(|(_, labels)| {
348 labels
349 .iter()
350 .find(|(key, _)| key == label_name)
351 .map(|(_, value)| value.clone())
352 })
353 .collect()
354 }
355 }
356
357 impl Recorder for CounterRegistrationRecorder {
358 fn describe_counter(&self, _key: KeyName, _unit: Option<Unit>, _description: SharedString) {
359 }
360
361 fn describe_gauge(&self, _key: KeyName, _unit: Option<Unit>, _description: SharedString) {}
362
363 fn describe_histogram(
364 &self,
365 _key: KeyName,
366 _unit: Option<Unit>,
367 _description: SharedString,
368 ) {
369 }
370
371 fn register_counter(&self, key: &Key, _metadata: &Metadata<'_>) -> Counter {
372 let labels = key
373 .labels()
374 .map(|label| (label.key().to_owned(), label.value().to_owned()))
375 .collect();
376
377 self.keys
378 .lock()
379 .expect("the test recorder mutex must not be poisoned")
380 .push((key.name().to_owned(), labels));
381
382 Counter::noop()
383 }
384
385 fn register_gauge(&self, _key: &Key, _metadata: &Metadata<'_>) -> Gauge {
386 Gauge::noop()
387 }
388
389 fn register_histogram(&self, _key: &Key, _metadata: &Metadata<'_>) -> Histogram {
390 Histogram::noop()
391 }
392 }
393
394 #[test]
395 fn user_agent_implementation_labels_are_bounded() {
396 assert_eq!(implementation_label("/Zakura:1.2.3/"), "zakura");
397 assert_eq!(implementation_label("/Zebra:5.1.0/"), "zebra");
398 assert_eq!(implementation_label("/MagicBean:6.2.0/"), "legacy_zcashd");
399 assert_eq!(implementation_label("/another-node:1.0/"), "other");
400 assert_eq!(implementation_label(""), "other");
401 }
402
403 #[test]
404 fn serialization_io_errors_keep_their_transport_meaning() {
405 let error: BoxError = HandshakeError::Serialization(SerializationError::Io(Arc::new(
406 io::Error::new(ErrorKind::ConnectionReset, "reset by test peer"),
407 )))
408 .into();
409
410 assert_eq!(
411 classify_connection_error(ConnectionDirection::Inbound, &error),
412 ConnectionErrorLabels {
413 stage: "handshake",
414 outcome: "connection_reset",
415 }
416 );
417 }
418
419 #[test]
420 fn tcp_errors_are_distinct_from_handshake_errors() {
421 let error: BoxError =
422 io::Error::new(ErrorKind::ConnectionRefused, "test listener is closed").into();
423
424 assert_eq!(
425 classify_connection_error(ConnectionDirection::Outbound, &error),
426 ConnectionErrorLabels {
427 stage: "tcp_connect",
428 outcome: "connection_refused",
429 }
430 );
431 }
432
433 #[tokio::test]
434 async fn outbound_outer_timeout_keeps_its_combined_stage() {
435 let service = tower::timeout::Timeout::new(
436 service_fn(|()| future::pending::<Result<(), BoxError>>()),
437 Duration::ZERO,
438 );
439 let error = service
440 .oneshot(())
441 .await
442 .expect_err("the pending test service must time out");
443
444 assert_eq!(
445 classify_connection_error(ConnectionDirection::Outbound, &error),
446 ConnectionErrorLabels {
447 stage: "tcp_or_handshake",
448 outcome: "timeout",
449 }
450 );
451 }
452
453 #[tokio::test]
454 async fn inbound_outer_timeout_is_a_handshake_timeout() {
455 let service = tower::timeout::Timeout::new(
456 service_fn(|()| future::pending::<Result<(), BoxError>>()),
457 Duration::ZERO,
458 );
459 let error = service
460 .oneshot(())
461 .await
462 .expect_err("the pending test service must time out");
463
464 assert_eq!(
465 classify_connection_error(ConnectionDirection::Inbound, &error),
466 ConnectionErrorLabels {
467 stage: "handshake",
468 outcome: "timeout",
469 }
470 );
471 }
472
473 #[tokio::test]
474 async fn tokio_timeout_is_a_handshake_timeout() {
475 let error: BoxError = tokio::time::timeout(Duration::ZERO, future::pending::<()>())
476 .await
477 .expect_err("the pending test future must time out")
478 .into();
479
480 assert_eq!(
481 classify_connection_error(ConnectionDirection::Inbound, &error),
482 ConnectionErrorLabels {
483 stage: "handshake",
484 outcome: "timeout",
485 }
486 );
487 }
488
489 #[test]
490 fn remote_version_outcomes_are_cancellation_safe() {
491 let recorder = CounterRegistrationRecorder::default();
492 let connected_addr = ConnectedAddr::InboundDirect {
493 addr: "127.0.0.1:8233"
494 .parse()
495 .expect("the test peer address must be valid"),
496 };
497
498 metrics::with_local_recorder(&recorder, || {
499 drop(RemoteVersionOutcomeGuard::new(
500 &Network::Mainnet,
501 &connected_addr,
502 "/Zakura:1.2.3/",
503 ));
504
505 RemoteVersionOutcomeGuard::new(&Network::Mainnet, &connected_addr, "/Zebra:3.0.0/")
506 .record_success();
507 });
508
509 assert_eq!(
510 recorder.label_values(REMOTE_VERSION_MESSAGES_TOTAL, "implementation"),
511 ["zakura", "zebra"]
512 );
513 assert_eq!(
514 recorder.label_values(REMOTE_VERSION_OUTCOMES_TOTAL, "outcome"),
515 ["canceled", "success"]
516 );
517 }
518}