Skip to main content

aranya_client/
client.rs

1//! Client-daemon connection.
2
3mod device;
4mod label;
5mod role;
6mod team;
7
8use std::{
9    fmt::Debug,
10    io,
11    path::Path,
12    time::{Duration, Instant},
13};
14
15use anyhow::Context as _;
16use aranya_crypto::{Csprng, Rng};
17#[doc(inline)]
18pub use aranya_daemon_api::ChanOp;
19use aranya_daemon_api::{
20    crypto::{
21        txp::{self, LengthDelimitedCodec},
22        PublicApiKey,
23    },
24    DaemonApiClient, Version, CS,
25};
26#[cfg(feature = "preview")]
27#[cfg_attr(docsrs, doc(cfg(feature = "preview")))]
28#[doc(inline)]
29pub use aranya_daemon_api::{
30    RoleManagementPerm as RoleManagementPermission, SimplePerm as Permission,
31};
32use aranya_util::{error::ReportExt, Addr};
33use tarpc::context;
34use tokio::{fs, net::UnixStream};
35use tracing::{debug, error, info};
36#[cfg(feature = "afc")]
37use {
38    crate::afc::{ChannelKeys as AfcChannelKeys, Channels as AfcChannels},
39    std::sync::Arc,
40};
41
42#[doc(inline)]
43#[expect(deprecated)]
44pub use self::device::KeyBundle;
45#[doc(inline)]
46pub use self::{
47    device::{Device, DeviceId, Devices, PublicKeyBundle},
48    label::{Label, LabelId, Labels},
49    role::{Role, RoleId, Roles},
50    team::{Team, TeamId},
51};
52use crate::{
53    config::{AddTeamConfig, CreateTeamConfig},
54    error::{self, aranya_error, InvalidArg, IpcError, Result},
55    util::ApiConv as _,
56};
57
58/// IPC timeout of 1 year (365 days).
59// A large value helps resolve IPC calls timing out when there are long-running
60// operations happening in the daemon.
61const IPC_TIMEOUT: Duration = Duration::from_secs(365 * 24 * 60 * 60);
62
63/// Builds a [`Client`].
64#[derive(Debug, Default)]
65pub struct ClientBuilder<'a> {
66    /// The UDS that the daemon is listening on.
67    #[cfg(unix)]
68    daemon_uds_path: Option<&'a Path>,
69}
70
71impl ClientBuilder<'_> {
72    /// Creates a new client builder.
73    pub fn new() -> Self {
74        Self::default()
75    }
76
77    /// Creates a client connection to the daemon.
78    ///
79    /// # Example
80    /// ```rust,no_run
81    /// use std::net::Ipv4Addr;
82    /// # use aranya_client::Client;
83    /// # #[tokio::main]
84    /// # async fn main() -> anyhow::Result<()> {
85    /// let client = Client::builder()
86    ///     .with_daemon_uds_path("/var/run/aranya/uds.sock".as_ref())
87    ///     .connect()
88    ///     .await?;
89    /// #    Ok(())
90    /// # }
91    pub async fn connect(self) -> Result<Client> {
92        let Some(uds_path) = self.daemon_uds_path else {
93            return Err(IpcError::new(InvalidArg::new(
94                "daemon_uds_path",
95                "must specify the daemon's UDS path",
96            ))
97            .into());
98        };
99
100        async {
101            info!(path = ?uds_path, "connecting to daemon");
102
103            let daemon = {
104                let pk = {
105                    // The public key is located next to the socket.
106                    let api_pk_path = uds_path.parent().unwrap_or(uds_path).join("api.pk");
107                    let bytes = fs::read(&api_pk_path)
108                        .await
109                        .with_context(|| "unable to read daemon API public key")
110                        .map_err(IpcError::new)?;
111                    PublicApiKey::<CS>::decode(&bytes)
112                        .context("unable to decode public API key")
113                        .map_err(IpcError::new)?
114                };
115
116                let uds_path = uds_path
117                    .canonicalize()
118                    .context("could not canonicalize uds_path")
119                    .map_err(error::other)?;
120                let sock = UnixStream::connect(&uds_path)
121                    .await
122                    .context("unable to connect to UDS path")
123                    .map_err(IpcError::new)?;
124                let info = uds_path.as_os_str().as_encoded_bytes();
125                let codec = LengthDelimitedCodec::builder()
126                    .max_frame_length(usize::MAX)
127                    .new_codec();
128                let transport = txp::client(sock, codec, Rng, pk, info);
129
130                DaemonApiClient::new(tarpc::client::Config::default(), transport).spawn()
131            };
132            debug!("connected to daemon");
133
134            let got = daemon
135                .version(create_ctx())
136                .await
137                .map_err(IpcError::new)?
138                .context("unable to retrieve daemon version")
139                .map_err(error::other)?;
140            let want = Version::parse(env!("CARGO_PKG_VERSION"))
141                .context("unable to parse `CARGO_PKG_VERSION`")
142                .map_err(error::other)?;
143            if got.major != want.major || got.minor != want.minor {
144                return Err(IpcError::new(io::Error::new(
145                    io::ErrorKind::Unsupported,
146                    format!("version mismatch: `{got}` != `{want}`"),
147                ))
148                .into());
149            }
150            debug!(client = ?want, daemon = ?got, "versions");
151
152            #[cfg(feature = "afc")]
153            let afc_keys = {
154                let afc_shm_info = daemon
155                    .afc_shm_info(create_ctx())
156                    .await
157                    .map_err(IpcError::new)?
158                    .context("unable to retrieve afc shm info")
159                    .map_err(error::other)?;
160                Arc::new(AfcChannelKeys::new(&afc_shm_info)?)
161            };
162
163            let client = Client {
164                daemon,
165                #[cfg(feature = "afc")]
166                afc_keys,
167            };
168
169            Ok(client)
170        }
171        .await
172        .inspect_err(
173            |err: &crate::Error| error!(error = %err.report(), "unable to connect to daemon"),
174        )
175    }
176}
177
178impl<'a> ClientBuilder<'a> {
179    /// Specifies the UDS socket path the daemon is listening on.
180    #[cfg(unix)]
181    #[cfg_attr(docsrs, doc(cfg(unix)))]
182    pub fn with_daemon_uds_path(mut self, sock: &'a Path) -> Self {
183        self.daemon_uds_path = Some(sock);
184        self
185    }
186}
187
188/// A client for invoking actions on and processing effects from
189/// the Aranya graph.
190///
191/// `Client` interacts with the [Aranya daemon] over
192/// a platform-specific IPC mechanism.
193///
194/// [Aranya daemon]: https://crates.io/crates/aranya-daemon
195#[derive(Debug)]
196pub struct Client {
197    /// RPC connection to the daemon
198    pub(crate) daemon: DaemonApiClient,
199    /// AFC channel keys.
200    #[cfg(feature = "afc")]
201    afc_keys: Arc<AfcChannelKeys>,
202}
203
204impl Client {
205    /// Returns a builder for `Client`.
206    pub fn builder<'a>() -> ClientBuilder<'a> {
207        ClientBuilder::new()
208    }
209
210    /// Returns the address that the Aranya sync server is bound to.
211    pub async fn local_addr(&self) -> Result<Addr> {
212        self.daemon
213            .aranya_local_addr(create_ctx())
214            .await
215            .map_err(IpcError::new)?
216            .map_err(aranya_error)
217    }
218
219    /// See [`Self::get_public_key_bundle`].
220    #[deprecated(note = "Use `get_public_key_bundle`")]
221    pub async fn get_key_bundle(&self) -> Result<PublicKeyBundle> {
222        self.get_public_key_bundle().await
223    }
224
225    /// Gets the public key bundle for this device.
226    pub async fn get_public_key_bundle(&self) -> Result<PublicKeyBundle> {
227        self.daemon
228            .get_public_key_bundle(create_ctx())
229            .await
230            .map_err(IpcError::new)?
231            .map_err(aranya_error)
232            .map(PublicKeyBundle::from_api)
233    }
234
235    /// Gets the public device ID for this device.
236    pub async fn get_device_id(&self) -> Result<DeviceId> {
237        self.daemon
238            .get_device_id(create_ctx())
239            .await
240            .map_err(IpcError::new)?
241            .map_err(aranya_error)
242            .map(DeviceId::from_api)
243    }
244
245    /// Create a new graph/team with the current device as the owner.
246    pub async fn create_team(&self, cfg: CreateTeamConfig) -> Result<Team<'_>> {
247        let team_id = self
248            .daemon
249            .create_team(create_ctx(), cfg.into())
250            .await
251            .map_err(IpcError::new)?
252            .map_err(aranya_error)
253            .map(TeamId::from_api)?;
254        Ok(Team {
255            client: self,
256            id: team_id.into_api(),
257        })
258    }
259
260    /// Generate random bytes from a CSPRNG.
261    /// Can be used to generate IKM for a generating a PSK seed.
262    pub async fn rand(&self, buf: &mut [u8]) {
263        <Rng as Csprng>::fill_bytes(&Rng, buf);
264    }
265
266    /// Get an existing team.
267    pub fn team(&self, team_id: TeamId) -> Team<'_> {
268        Team {
269            client: self,
270            id: team_id.into_api(),
271        }
272    }
273
274    /// Add a team to local device storage.
275    pub async fn add_team(&self, cfg: AddTeamConfig) -> Result<Team<'_>> {
276        let cfg = aranya_daemon_api::AddTeamConfig::from(cfg);
277        let team_id = TeamId::from_api(cfg.team_id);
278
279        self.daemon
280            .add_team(create_ctx(), cfg)
281            .await
282            .map_err(IpcError::new)?
283            .map_err(aranya_error)?;
284        Ok(Team {
285            client: self,
286            id: team_id.into_api(),
287        })
288    }
289
290    /// Remove a team from local device storage.
291    pub async fn remove_team(&self, team_id: TeamId) -> Result<()> {
292        self.daemon
293            .remove_team(create_ctx(), team_id.into_api())
294            .await
295            .map_err(IpcError::new)?
296            .map_err(aranya_error)
297    }
298
299    /// Get access to Aranya Fast Channels.
300    #[cfg(feature = "afc")]
301    #[cfg_attr(docsrs, doc(cfg(feature = "afc")))]
302    pub fn afc(&self) -> AfcChannels {
303        AfcChannels::new(self.daemon.clone(), self.afc_keys.clone())
304    }
305}
306
307/// Returns the current [`Context`](context::Context) with a deadline set to the current time
308/// plus [`IPC_TIMEOUT`].
309pub(crate) fn create_ctx() -> context::Context {
310    let mut ctx = context::current();
311    ctx.deadline = Instant::now()
312        .checked_add(IPC_TIMEOUT)
313        .expect("IPC_TIMEOUT should not overflow");
314
315    ctx
316}