hashiverse_server_lib/tools/tools.rs
1//! # Server-only helpers
2//!
3//! A small grab bag of utilities the server needs that don't belong in
4//! `hashiverse-lib`:
5//!
6//! - [`get_public_ipv4`] — asks ipify.org for the node's externally-visible IPv4 so
7//! the node's advertised peer address matches reality (behind NAT / firewalls).
8//! Falls back to loopback when explicitly running in a local-only test network.
9//! - [`is_ssrf_protected_ip`] — the deny-list for every server-initiated outbound
10//! fetch (URL previews, Let's Encrypt challenges, ipify). Rejects loopback,
11//! RFC-1918 private ranges, link-local, CGNAT, cloud-metadata endpoints
12//! (169.254.169.254 friends), multicast, broadcast, the unspecified address,
13//! IPv6 ULA/link-local, and IPv4-mapped IPv6. This is the primary defence against
14//! SSRF — a malicious URL in a post must not let a client coerce our server into
15//! probing its own internal network.
16//! - [`spawn_ctrl_c_handler`] — wires `ctrl-c` into a `CancellationToken` so the
17//! server can shut down gracefully.
18
19use log::warn;
20use tokio_util::sync::CancellationToken;
21
22pub async fn get_public_ipv4(force_local_network: bool) -> anyhow::Result<String> {
23 match force_local_network {
24 true => Ok("127.0.0.1".to_string()),
25 false => {
26 let client = reqwest::ClientBuilder::new().danger_accept_invalid_certs(true).build()?; // we are willing to accept invalid certs as we do application level signature checking
27 let response = client.get("https://api4.ipify.org").send().await?.text().await?;
28 Ok(response)
29 }
30 }
31}
32
33pub fn spawn_ctrl_c_handler(cancellation_token: CancellationToken) {
34 tokio::spawn(async move {
35 if tokio::signal::ctrl_c().await.is_ok() {
36 warn!("Ctrl+C received, cancelling...");
37 cancellation_token.cancel();
38 }
39 });
40}
41
42/// Returns true for any IP that must never be the target of a server-initiated fetch.
43/// Covers: loopback, RFC-1918 private, link-local (incl. cloud metadata 169.254.169.254),
44/// CGNAT (100.64.0.0/10), multicast, unspecified, broadcast, IPv6 ULA (fc00::/7),
45/// IPv6 link-local (fe80::/10), and IPv4-mapped IPv6 addresses whose embedded v4 is protected.
46pub fn is_ssrf_protected_ip(ip: std::net::IpAddr) -> bool {
47 match ip {
48 std::net::IpAddr::V4(v4) => {
49 let o = v4.octets();
50 v4.is_loopback() // 127.0.0.0/8
51 || v4.is_private() // 10/8, 172.16/12, 192.168/16
52 || v4.is_link_local() // 169.254.0.0/16 — cloud metadata
53 || v4.is_multicast() // 224.0.0.0/4
54 || v4.is_unspecified() // 0.0.0.0
55 || v4.is_broadcast() // 255.255.255.255
56 || (o[0] == 100 && (o[1] & 0xC0) == 64) // CGNAT 100.64.0.0/10
57 }
58 std::net::IpAddr::V6(v6) => {
59 let s = v6.segments();
60 v6.is_loopback() // ::1
61 || v6.is_multicast() // ff00::/8
62 || v6.is_unspecified() // ::
63 || (s[0] & 0xFFC0) == 0xFE80 // link-local fe80::/10
64 || (s[0] & 0xFE00) == 0xFC00 // unique local fc00::/7
65 // IPv4-mapped (::ffff:0:0/96) — recurse on the embedded v4 address
66 || matches!(v6.to_ipv4_mapped(), Some(v4) if is_ssrf_protected_ip(std::net::IpAddr::V4(v4)))
67 }
68 }
69}