1use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, ToSocketAddrs as _};
2
3use url::Url;
4
5pub fn source_ip() -> anyhow::Result<IpAddr> {
9 source_ip_for(&Url::parse("udp://k.root-servers.net:53").unwrap())
10}
11
12pub 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}