1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
use std::net::Ipv6Addr;

use futures::future::join_all;
use hickory_resolver::{
    error::ResolveError, proto::rr::rdata::AAAA, IntoName, Name, TokioAsyncResolver,
};

use crate::{
    error::Error,
    resolver::{lookup_txt, Resolver},
    Node, Peer, Region,
};

/// Query the Fly.io [internal DNS][] records for a particular app.
///
/// [internal DNS]: https://fly.io/docs/reference/private-networking/#fly-internal-addresses
pub struct AppResolver<'r> {
    domain: Name,
    resolver: &'r TokioAsyncResolver,
}

impl<'r> AppResolver<'r> {
    pub(crate) fn new(app: impl Into<String>, resolver: &'r Resolver) -> Self {
        let app: String = app.into();
        let name = Name::from_ascii(app).expect("invalid app name");
        let domain = name.append_label("internal").unwrap();

        Self {
            domain,
            resolver: &resolver.0,
        }
    }

    /// Find the Fly.io regions where this app is deployed.
    #[cfg(feature = "regions")]
    #[cfg_attr(docsrs, doc(cfg(feature = "regions")))]
    pub async fn regions(&self) -> Result<Vec<Region>, Error> {
        let value = self.txt("regions").await?;

        Ok(value
            .split(',')
            .filter_map(|code| code.parse::<Region>().ok())
            .collect())
    }

    /// Find all running instances of this Fly.io app.
    pub async fn nodes(&self) -> Result<Vec<Node>, Error> {
        let value = self.txt("vms").await?;

        value.split(',').map(|peer| peer.parse::<Node>()).collect()
    }

    /// Find all running [instances][AppResolver::nodes] of this Fly.io app, and
    /// resolve all their instance ID’s to private IP addresses.
    pub async fn peers(&self) -> Result<Vec<Peer>, Error> {
        let nodes = self.nodes().await?;

        let addrs = join_all(nodes.iter().map(|node| {
            let name = Name::from_ascii(&node.id)
                .expect("invalid node ID")
                .append_label("vm")
                .unwrap()
                .append_domain(&self.domain)
                .expect("invalid query");

            self.resolver.ipv6_lookup(name)
        }))
        .await
        .into_iter()
        .collect::<Result<Vec<_>, ResolveError>>()
        .map_err(Error::from)?;

        Ok(nodes
            .into_iter()
            .zip(addrs.into_iter())
            .filter_map(|(node, addrs)| {
                if let Some(AAAA(addr)) = addrs.into_iter().next() {
                    Some(node.into_peer(addr))
                } else {
                    None
                }
            })
            .collect())
    }

    /// Find the geographically-nearest _n_ instances of this Fly.io app.
    pub async fn nearest_peer_addresses(&self, n: usize) -> Result<Vec<Ipv6Addr>, Error> {
        let top = Name::from_ascii(format!("top{n}"))
            .expect("invalid top n")
            .append_label("nearest")
            .unwrap()
            .append_label("of")
            .unwrap()
            .append_domain(&self.domain)
            .expect("invalid query");

        let results = self.resolver.ipv6_lookup(top).await.map_err(Error::from)?;

        Ok(results.into_iter().map(|r| r.0).collect())
    }

    /// Perform an arbitrary `TXT` record query on the `<app>.internal` domain.
    pub async fn txt(&self, name: impl IntoName) -> Result<String, Error> {
        let query = name
            .into_name()
            .expect("invalid name")
            .append_domain(&self.domain)
            .expect("invalid app domain");

        lookup_txt(self.resolver, query).await
    }
}