@@ -35,14 +35,15 @@ use tokio::{
35
35
} ;
36
36
use tokio_util:: task:: AbortOnDropHandle ;
37
37
use tracing:: { debug, debug_span, error, info_span, trace, warn, Instrument , Span } ;
38
+ use url:: Host ;
38
39
39
40
use super :: NetcheckMetrics ;
40
41
use crate :: {
41
42
defaults:: DEFAULT_STUN_PORT ,
42
43
dns:: { DnsResolver , ResolverExt } ,
43
44
netcheck:: { self , Report } ,
44
45
ping:: { PingError , Pinger } ,
45
- relay:: { RelayMap , RelayNode , RelayUrl } ,
46
+ relay:: { http :: RELAY_PROBE_PATH , RelayMap , RelayNode , RelayUrl } ,
46
47
stun,
47
48
util:: MaybeFuture ,
48
49
} ;
@@ -466,6 +467,7 @@ impl Actor {
466
467
. as_ref ( )
467
468
. and_then ( |l| l. preferred_relay . clone ( ) ) ;
468
469
470
+ let dns_resolver = self . dns_resolver . clone ( ) ;
469
471
let dm = self . relay_map . clone ( ) ;
470
472
self . outstanding_tasks . captive_task = true ;
471
473
MaybeFuture {
@@ -474,7 +476,7 @@ impl Actor {
474
476
debug ! ( "Captive portal check started after {CAPTIVE_PORTAL_DELAY:?}" ) ;
475
477
let captive_portal_check = tokio:: time:: timeout (
476
478
CAPTIVE_PORTAL_TIMEOUT ,
477
- check_captive_portal ( & dm, preferred_relay)
479
+ check_captive_portal ( & dns_resolver , & dm, preferred_relay)
478
480
. instrument ( debug_span ! ( "captive-portal" ) ) ,
479
481
) ;
480
482
match captive_portal_check. await {
@@ -743,7 +745,7 @@ async fn run_probe(
743
745
}
744
746
Probe :: Https { ref node, .. } => {
745
747
debug ! ( "sending probe HTTPS" ) ;
746
- match measure_https_latency ( node) . await {
748
+ match measure_https_latency ( & dns_resolver , node, None ) . await {
747
749
Ok ( ( latency, ip) ) => {
748
750
result. latency = Some ( latency) ;
749
751
// We set these IPv4 and IPv6 but they're not really used
@@ -853,7 +855,11 @@ async fn run_stun_probe(
853
855
/// return a "204 No Content" response and checking if that's what we get.
854
856
///
855
857
/// The boolean return is whether we think we have a captive portal.
856
- async fn check_captive_portal ( dm : & RelayMap , preferred_relay : Option < RelayUrl > ) -> Result < bool > {
858
+ async fn check_captive_portal (
859
+ dns_resolver : & DnsResolver ,
860
+ dm : & RelayMap ,
861
+ preferred_relay : Option < RelayUrl > ,
862
+ ) -> Result < bool > {
857
863
// If we have a preferred relay node and we can use it for non-STUN requests, try that;
858
864
// otherwise, pick a random one suitable for non-STUN requests.
859
865
let preferred_relay = preferred_relay. and_then ( |url| match dm. get_node ( & url) {
@@ -881,9 +887,23 @@ async fn check_captive_portal(dm: &RelayMap, preferred_relay: Option<RelayUrl>)
881
887
}
882
888
} ;
883
889
884
- let client = reqwest:: ClientBuilder :: new ( )
885
- . redirect ( reqwest:: redirect:: Policy :: none ( ) )
886
- . build ( ) ?;
890
+ let mut builder = reqwest:: ClientBuilder :: new ( ) . redirect ( reqwest:: redirect:: Policy :: none ( ) ) ;
891
+ if let Some ( Host :: Domain ( domain) ) = url. host ( ) {
892
+ // Use our own resolver rather than getaddrinfo
893
+ //
894
+ // For some reason reqwest wants SocketAddr rather than IpAddr but then proceeds to
895
+ // ignore the port, extracting it from the URL instead. We supply a dummy port.
896
+ //
897
+ // Ideally we would try to resolve **both** IPv4 and IPv6 rather than purely race
898
+ // them. But our resolver doesn't support that yet.
899
+ let addrs: Vec < _ > = dns_resolver
900
+ . lookup_ipv4_ipv6_staggered ( domain, DNS_TIMEOUT , DNS_STAGGERING_MS )
901
+ . await ?
902
+ . map ( |ipaddr| SocketAddr :: new ( ipaddr, 80 ) )
903
+ . collect ( ) ;
904
+ builder = builder. resolve_to_addrs ( domain, & addrs) ;
905
+ }
906
+ let client = builder. build ( ) ?;
887
907
888
908
// Note: the set of valid characters in a challenge and the total
889
909
// length is limited; see is_challenge_char in bin/iroh-relay for more
@@ -1023,33 +1043,72 @@ async fn run_icmp_probe(
1023
1043
Ok ( report)
1024
1044
}
1025
1045
1046
+ /// Executes an HTTPS probe.
1047
+ ///
1048
+ /// If `certs` is provided they will be added to the trusted root certificates, allowing the
1049
+ /// use of self-signed certificates for servers. Currently this is used for testing.
1026
1050
#[ allow( clippy:: unused_async) ]
1027
- async fn measure_https_latency ( _node : & RelayNode ) -> Result < ( Duration , IpAddr ) > {
1028
- bail ! ( "not implemented" ) ;
1029
- // TODO:
1030
- // - needs relayhttp::Client
1031
- // - measurement hooks to measure server processing time
1032
-
1033
- // metricHTTPSend.Add(1)
1034
- // let ctx, cancel := context.WithTimeout(httpstat.WithHTTPStat(ctx, &result), overallProbeTimeout);
1035
- // let dc := relayhttp.NewNetcheckClient(c.logf);
1036
- // let tlsConn, tcpConn, node := dc.DialRegionTLS(ctx, reg)?;
1037
- // if ta, ok := tlsConn.RemoteAddr().(*net.TCPAddr);
1038
- // req, err := http.NewRequestWithContext(ctx, "GET", "https://"+node.HostName+"/relay/latency-check", nil);
1039
- // resp, err := hc.Do(req);
1040
-
1041
- // // relays should give us a nominal status code, so anything else is probably
1042
- // // an access denied by a MITM proxy (or at the very least a signal not to
1043
- // // trust this latency check).
1044
- // if resp.StatusCode > 299 {
1045
- // return 0, ip, fmt.Errorf("unexpected status code: %d (%s)", resp.StatusCode, resp.Status)
1046
- // }
1047
- // _, err = io.Copy(io.Discard, io.LimitReader(resp.Body, 8<<10));
1048
- // result.End(c.timeNow())
1049
-
1050
- // // TODO: decide best timing heuristic here.
1051
- // // Maybe the server should return the tcpinfo_rtt?
1052
- // return result.ServerProcessing, ip, nil
1051
+ async fn measure_https_latency (
1052
+ dns_resolver : & DnsResolver ,
1053
+ node : & RelayNode ,
1054
+ certs : Option < Vec < rustls:: pki_types:: CertificateDer < ' static > > > ,
1055
+ ) -> Result < ( Duration , IpAddr ) > {
1056
+ let url = node. url . join ( RELAY_PROBE_PATH ) ?;
1057
+
1058
+ // This should also use same connection establishment as relay client itself, which
1059
+ // needs to be more configurable so users can do more crazy things:
1060
+ // https://github.com/n0-computer/iroh/issues/2901
1061
+ let mut builder = reqwest:: ClientBuilder :: new ( ) . redirect ( reqwest:: redirect:: Policy :: none ( ) ) ;
1062
+ if let Some ( Host :: Domain ( domain) ) = url. host ( ) {
1063
+ // Use our own resolver rather than getaddrinfo
1064
+ //
1065
+ // For some reason reqwest wants SocketAddr rather than IpAddr but then proceeds to
1066
+ // ignore the port, extracting it from the URL instead. We supply a dummy port.
1067
+ //
1068
+ // The relay Client uses `.lookup_ipv4_ipv6` to connect, so use the same function
1069
+ // but staggered for reliability. Ideally this tries to resolve **both** IPv4 and
1070
+ // IPv6 though. But our resolver does not have a function for that yet.
1071
+ let addrs: Vec < _ > = dns_resolver
1072
+ . lookup_ipv4_ipv6_staggered ( domain, DNS_TIMEOUT , DNS_STAGGERING_MS )
1073
+ . await ?
1074
+ . map ( |ipaddr| SocketAddr :: new ( ipaddr, 80 ) )
1075
+ . collect ( ) ;
1076
+ builder = builder. resolve_to_addrs ( domain, & addrs) ;
1077
+ }
1078
+ if let Some ( certs) = certs {
1079
+ for cert in certs {
1080
+ let cert = reqwest:: Certificate :: from_der ( & cert) ?;
1081
+ builder = builder. add_root_certificate ( cert) ;
1082
+ }
1083
+ }
1084
+ let client = builder. build ( ) ?;
1085
+
1086
+ let start = Instant :: now ( ) ;
1087
+ let mut response = client. request ( reqwest:: Method :: GET , url) . send ( ) . await ?;
1088
+ let latency = start. elapsed ( ) ;
1089
+ if response. status ( ) . is_success ( ) {
1090
+ // Drain the response body to be nice to the server, up to a limit.
1091
+ const MAX_BODY_SIZE : usize = 8 << 10 ; // 8 KiB
1092
+ let mut body_size = 0 ;
1093
+ while let Some ( chunk) = response. chunk ( ) . await ? {
1094
+ body_size += chunk. len ( ) ;
1095
+ if body_size >= MAX_BODY_SIZE {
1096
+ break ;
1097
+ }
1098
+ }
1099
+
1100
+ // Only `None` if a different hyper HttpConnector in the request.
1101
+ let remote_ip = response
1102
+ . remote_addr ( )
1103
+ . context ( "missing HttpInfo from HttpConnector" ) ?
1104
+ . ip ( ) ;
1105
+ Ok ( ( latency, remote_ip) )
1106
+ } else {
1107
+ Err ( anyhow ! (
1108
+ "Error response from server: '{}'" ,
1109
+ response. status( ) . canonical_reason( ) . unwrap_or_default( )
1110
+ ) )
1111
+ }
1053
1112
}
1054
1113
1055
1114
/// Updates a netcheck [`Report`] with a new [`ProbeReport`].
@@ -1118,8 +1177,13 @@ fn update_report(report: &mut Report, probe_report: ProbeReport) {
1118
1177
mod tests {
1119
1178
use std:: net:: { Ipv4Addr , Ipv6Addr } ;
1120
1179
1180
+ use testresult:: TestResult ;
1181
+
1121
1182
use super :: * ;
1122
- use crate :: defaults:: staging:: { default_eu_relay_node, default_na_relay_node} ;
1183
+ use crate :: {
1184
+ defaults:: staging:: { default_eu_relay_node, default_na_relay_node} ,
1185
+ test_utils,
1186
+ } ;
1123
1187
1124
1188
#[ test]
1125
1189
fn test_update_report_stun_working ( ) {
@@ -1368,4 +1432,28 @@ mod tests {
1368
1432
panic ! ( "Ping error: {err:#}" ) ;
1369
1433
}
1370
1434
}
1435
+
1436
+ #[ tokio:: test]
1437
+ async fn test_measure_https_latency ( ) -> TestResult {
1438
+ let _logging_guard = iroh_test:: logging:: setup ( ) ;
1439
+ let ( _relay_map, relay_url, server) = test_utils:: run_relay_server ( ) . await ?;
1440
+ let dns_resolver = crate :: dns:: resolver ( ) ;
1441
+ warn ! ( ?relay_url, "RELAY_URL" ) ;
1442
+ let node = RelayNode {
1443
+ stun_only : false ,
1444
+ stun_port : 0 ,
1445
+ url : relay_url. clone ( ) ,
1446
+ } ;
1447
+ let ( latency, ip) =
1448
+ measure_https_latency ( dns_resolver, & node, server. certificates ( ) ) . await ?;
1449
+
1450
+ assert ! ( latency > Duration :: ZERO ) ;
1451
+
1452
+ let relay_url_ip = relay_url
1453
+ . host_str ( )
1454
+ . context ( "host" ) ?
1455
+ . parse :: < std:: net:: IpAddr > ( ) ?;
1456
+ assert_eq ! ( ip, relay_url_ip) ;
1457
+ Ok ( ( ) )
1458
+ }
1371
1459
}
0 commit comments