Skip to main content

pubhubs/misc/
net_ext.rs

1use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, ToSocketAddrs as _};
2
3use url::Url;
4
5/// Determines the IP address of the network interface used by this host to contact the broader internet.
6///
7/// This is likely a private IP only reachable via the local network (if behind a NAT).
8pub fn source_ip() -> anyhow::Result<IpAddr> {
9    source_ip_for(&Url::parse("udp://k.root-servers.net:53").unwrap())
10}
11
12/// Determines which [`IpAddr`] is used to contact the given address encoded in a [`Url`] with as
13/// scheme either `tcp` or `udp`.  The _port_ of the url must be specified, but the _fragment_,
14/// _query_, _username_ and _password_ cannot be set, and _path_ must be trivial.
15pub fn source_ip_for(url: &Url) -> anyhow::Result<IpAddr> {
16    anyhow::ensure!(
17        url.fragment().is_none(),
18        "this url cannot contain fragment (i.e. '#')"
19    );
20    anyhow::ensure!(
21        url.query().is_none(),
22        "this url cannot contain query (i.e. '?')",
23    );
24    anyhow::ensure!(url.password().is_none(), "this url cannot contain password",);
25    anyhow::ensure!(url.username() == "", "this url cannot contain username",);
26    anyhow::ensure!(
27        matches!(url.path(), "" | "/"),
28        "this url must have a trivial path",
29    );
30
31    let port: u16 = url
32        .port()
33        .ok_or_else(|| anyhow::anyhow!("this url must contain a port number"))?;
34
35    let sas: Vec<SocketAddr> = match url.host() {
36        None => anyhow::bail!("this url must contain a host"),
37        Some(url::Host::Ipv4(ip)) => (ip, port).to_socket_addrs()?.collect(),
38        Some(url::Host::Ipv6(ip)) => (ip, port).to_socket_addrs()?.collect(),
39        Some(url::Host::Domain(domain)) => (domain, port).to_socket_addrs()?.collect(),
40    };
41
42    for sa in sas {
43        match source_ip_for_sa(sa, url.scheme()) {
44            Ok(ip) => return Ok(ip),
45            Err(err) => {
46                log::warn!(
47                    "failed to get source ip address for contacting port {port} of {:?}: {err}",
48                    url.host()
49                );
50            }
51        }
52    }
53
54    anyhow::bail!(
55        "could not obtain a source ip address for any of the ip addresses associated to {:?}",
56        url.host()
57    );
58}
59
60fn source_ip_for_sa(sa: SocketAddr, scheme: &str) -> anyhow::Result<IpAddr> {
61    let unspecified_ip: IpAddr = if sa.is_ipv4() {
62        Ipv4Addr::UNSPECIFIED.into()
63    } else {
64        Ipv6Addr::UNSPECIFIED.into()
65    };
66
67    match scheme {
68        "udp" => {
69            let sock = std::net::UdpSocket::bind((unspecified_ip, 0))?;
70            sock.connect(sa)?;
71            Ok(sock.local_addr()?.ip())
72        }
73        "tcp" => {
74            let stream = std::net::TcpStream::connect(sa)?;
75            Ok(stream.local_addr()?.ip())
76        }
77        _ => anyhow::bail!("invalid url scheme '{}'; must be 'tcp' or 'udp'", scheme),
78    }
79}