Skip to content

Commit 1357179

Browse files
committed
fix: remove final dot in relay urls when doing https requests
1 parent 9f00137 commit 1357179

File tree

3 files changed

+59
-7
lines changed

3 files changed

+59
-7
lines changed

iroh-base/src/relay_url.rs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,43 @@ impl From<Url> for RelayUrl {
3838
}
3939
}
4040

41+
impl RelayUrl {
42+
/// Returns the URL while removing the final dot in the relay URL's domain name.
43+
///
44+
/// By default, we add a final dot to relay URLs to make sure that DNS resolution always
45+
/// considers them as top-level domains without appending a search suffix. When using
46+
/// the URL for TLS hostname verification, usually a domain name without a final
47+
/// dot is expected. So when using the URL in the context of HTTPS or TLS, use
48+
/// this function to get the URL without the final dot.
49+
pub fn without_final_dot(&self) -> Url {
50+
let mut url = self.0.deref().clone();
51+
if let Some(domain) = url.domain() {
52+
if let Some(stripped) = domain.strip_suffix('.') {
53+
let stripped = stripped.to_string();
54+
url.set_host(Some(&stripped)).ok();
55+
}
56+
}
57+
url
58+
}
59+
60+
/// Return the string representation of the host (domain or IP address) for this URL, if any.
61+
///
62+
/// If the host is a domain, and the domain ends with a final dot, the final dot is removed.
63+
///
64+
/// See [`Self::without_final_dot`] for details on when you might want to use this.
65+
pub fn host_str_without_final_dot(&self) -> Option<&str> {
66+
if let Some(domain) = self.0.domain() {
67+
if let Some(stripped) = domain.strip_suffix('.') {
68+
Some(stripped)
69+
} else {
70+
Some(domain)
71+
}
72+
} else {
73+
self.0.host_str()
74+
}
75+
}
76+
}
77+
4178
/// Can occur when parsing a string into a [`RelayUrl`].
4279
#[stack_error(derive, add_meta)]
4380
#[error("Failed to parse relay URL")]
@@ -124,5 +161,16 @@ mod tests {
124161

125162
let url3 = RelayUrl::from(Url::parse("https://example.com/").unwrap());
126163
assert_eq!(url, url3);
164+
165+
// tests `RelayUrl::without_final_dot`
166+
assert_eq!(url.deref(), &Url::parse("https://example.com.").unwrap());
167+
assert_eq!(
168+
url.without_final_dot(),
169+
Url::parse("https://example.com").unwrap()
170+
);
171+
172+
// tests `RelayUrl::host_str_without_final_dot`
173+
assert_eq!(url.host_str(), Some("example.com."));
174+
assert_eq!(url.host_str_without_final_dot(), Some("example.com"));
127175
}
128176
}

iroh/src/net_report.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -779,7 +779,7 @@ async fn run_probe_v4(
779779
debug!(?relay_addr_orig, ?relay_addr, "relay addr v4");
780780
let host = relay
781781
.url
782-
.host_str()
782+
.host_str_without_final_dot()
783783
.ok_or_else(|| e!(QadProbeError::MissingHost))?;
784784
let conn = quic_client
785785
.create_conn(relay_addr, host)
@@ -853,7 +853,7 @@ async fn run_probe_v6(
853853
debug!(?relay_addr_orig, ?relay_addr, "relay addr v6");
854854
let host = relay
855855
.url
856-
.host_str()
856+
.host_str_without_final_dot()
857857
.ok_or_else(|| e!(QadProbeError::MissingHost))?;
858858
let conn = quic_client
859859
.create_conn(relay_addr, host)

iroh/src/net_report/reportgen.rs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -606,7 +606,7 @@ async fn check_captive_portal(
606606
// length is limited; see is_challenge_char in bin/iroh-relay for more
607607
// details.
608608

609-
let host_name = url.host_str().unwrap_or_default();
609+
let host_name = url.host_str_without_final_dot().unwrap_or_default();
610610
let challenge = format!("ts_{host_name}");
611611
let portal_url = format!("http://{host_name}/generate_204");
612612
let res = client
@@ -789,7 +789,9 @@ async fn run_https_probe(
789789
#[cfg(any(test, feature = "test-utils"))] insecure_skip_relay_cert_verify: bool,
790790
) -> Result<HttpsProbeReport, MeasureHttpsLatencyError> {
791791
trace!("HTTPS probe start");
792-
let url = relay.join(RELAY_PROBE_PATH)?;
792+
// Convert the relay URL to a URL that has no final dot, because the final dot
793+
// may trip up the certificate verification.
794+
let url = relay.without_final_dot().join(RELAY_PROBE_PATH)?;
793795

794796
// This should also use same connection establishment as relay client itself, which
795797
// needs to be more configurable so users can do more crazy things:
@@ -802,7 +804,9 @@ async fn run_https_probe(
802804
}
803805

804806
#[cfg(not(wasm_browser))]
805-
if let Some(Host::Domain(domain)) = url.host() {
807+
if let (Some(Host::Domain(domain_for_dns)), Some(Host::Domain(domain_for_reqwest))) =
808+
(relay.host(), url.host())
809+
{
806810
// Use our own resolver rather than getaddrinfo
807811
//
808812
// Be careful, a non-zero port will override the port in the URI.
@@ -811,12 +815,12 @@ async fn run_https_probe(
811815
// but staggered for reliability. Ideally this tries to resolve **both** IPv4 and
812816
// IPv6 though. But our resolver does not have a function for that yet.
813817
let addrs: Vec<_> = dns_resolver
814-
.lookup_ipv4_ipv6_staggered(domain, DNS_TIMEOUT, DNS_STAGGERING_MS)
818+
.lookup_ipv4_ipv6_staggered(domain_for_dns, DNS_TIMEOUT, DNS_STAGGERING_MS)
815819
.await?
816820
.map(|ipaddr| SocketAddr::new(ipaddr, 0))
817821
.collect();
818822
trace!(?addrs, "resolved addrs");
819-
builder = builder.resolve_to_addrs(domain, &addrs);
823+
builder = builder.resolve_to_addrs(domain_for_reqwest, &addrs);
820824
}
821825

822826
#[cfg(all(not(wasm_browser), any(test, feature = "test-utils")))]

0 commit comments

Comments
 (0)