Skip to main content

snap_control/
client.rs

1// Copyright 2025 Anapaya Systems
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//   http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14//! Connect RPC client for the SNAP control plane API.
15
16use std::{net::SocketAddr, ops::Deref, sync::Arc};
17
18use async_trait::async_trait;
19use endhost_api_client::client::CrpcEndhostApiClient;
20use reqwest_connect_rpc::{
21    client::{CrpcClientCreationError, CrpcClientError},
22    token_source::TokenSource,
23};
24use snap_tun::client::SnapTunControlPlaneClient;
25use url::Url;
26use x25519_dalek::PublicKey;
27
28use crate::{
29    api::crpc::{GET_SNAP_DATA_PLANE_ADDRESS, REGISTER_SNAPTUN_IDENTITY, SERVICE_PATH},
30    proto::anapaya::snap::v1 as proto,
31};
32
33/// Re-export the endhost API client and the reqwest connect RPC cllient.
34pub mod re_export {
35    pub use endhost_api_client::client::{CrpcEndhostApiClient, EndhostApiClient};
36    pub use reqwest_connect_rpc::{
37        client::{CrpcClientCreationError, CrpcClientError},
38        token_source::*,
39    };
40}
41
42/// SNAP data plane address response.
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct GetDataPlaneAddressResponse {
45    /// The UDP endpoint (host:port) of the SNAP data plane.
46    pub address: SocketAddr,
47    /// The URL of the SNAPtun control plane API. This can be the same as the data plane address.
48    /// XXX(uniquefine): Make this required once all servers have been updated.
49    pub snap_tun_control_address: Option<Url>,
50    /// The static identity of the snaptun-ng server.
51    /// XXX(uniquefine): Make this required once all servers have been updated.
52    pub snap_static_x25519: Option<PublicKey>,
53}
54
55/// SNAP control plane API trait.
56#[async_trait]
57pub trait ControlPlaneApi: Send + Sync {
58    /// Get the SNAP data plane address.
59    async fn get_data_plane_address(&self) -> Result<GetDataPlaneAddressResponse, CrpcClientError>;
60
61    /// Register a static identity for a snaptun connection.
62    async fn register_snaptun_identity(
63        &self,
64        initiator_identity: PublicKey,
65        psk_share: Option<[u8; 32]>,
66    ) -> Result<Option<[u8; 32]>, CrpcClientError>;
67}
68
69/// Connect RPC client for the SNAP control plane API.
70pub struct CrpcSnapControlClient {
71    client: CrpcEndhostApiClient,
72}
73
74impl Deref for CrpcSnapControlClient {
75    type Target = CrpcEndhostApiClient;
76
77    fn deref(&self) -> &Self::Target {
78        &self.client
79    }
80}
81
82impl CrpcSnapControlClient {
83    /// Creates a new client with default settings
84    pub fn new(base_url: &Url) -> Result<Self, CrpcClientCreationError> {
85        let client = CrpcEndhostApiClient::new(base_url)?;
86        Ok(Self { client })
87    }
88
89    /// Creates a new client with the provided `reqwest::Client`.
90    pub fn new_with_client(
91        base_url: &Url,
92        client: reqwest::Client,
93    ) -> Result<Self, CrpcClientCreationError> {
94        Ok(Self {
95            client: CrpcEndhostApiClient::new_with_client(base_url, client)?,
96        })
97    }
98
99    /// Uses the provided token source for authentication.
100    pub fn use_token_source(&mut self, token_source: Arc<dyn TokenSource>) -> &mut Self {
101        self.client.use_token_source(token_source);
102        self
103    }
104}
105
106#[async_trait]
107impl ControlPlaneApi for CrpcSnapControlClient {
108    async fn get_data_plane_address(&self) -> Result<GetDataPlaneAddressResponse, CrpcClientError> {
109        let res: proto::GetSnapDataPlaneAddressResponse = self
110            .client
111            .unary_request::<proto::GetSnapDataPlaneAddressRequest, proto::GetSnapDataPlaneAddressResponse>(
112                &format!("{SERVICE_PATH}{GET_SNAP_DATA_PLANE_ADDRESS}"),
113                &proto::GetSnapDataPlaneAddressRequest::default(),
114            )
115            .await?;
116        let address = res.address.parse().map_err(|e: std::net::AddrParseError| {
117            CrpcClientError::DecodeError {
118                context: "parsing data plane address".into(),
119                source: Some(e.into()),
120                body: None,
121            }
122        })?;
123
124        let snap_tun_control_address = res
125            .snap_tun_control_address
126            .map(|address| {
127                // Try to parse the address as a URL first.
128                if let Ok(url) = Url::parse(&address) {
129                    return Ok(url);
130                }
131                match address.parse::<SocketAddr>() {
132                    Ok(addr) => {
133                        let mut u = Url::parse("http://.").unwrap();
134                        let _ = u.set_ip_host(addr.ip());
135                        let _ = u.set_port(Some(addr.port()));
136                        Ok(u)
137                    }
138                    Err(e) => {
139                        Err(CrpcClientError::DecodeError {
140                            context: "parsing server control address".into(),
141                            source: Some(e.into()),
142                            body: None,
143                        })
144                    }
145                }
146            })
147            .transpose()?;
148        let snap_static_x25519 = res
149            .snap_static_x25519
150            .map(|key| {
151                let key_bytes: [u8; 32] =
152                    key.as_slice()
153                        .try_into()
154                        .map_err(|e: std::array::TryFromSliceError| {
155                            CrpcClientError::DecodeError {
156                                context: "server static identity is not 32 bytes".into(),
157                                source: Some(e.into()),
158                                body: None,
159                            }
160                        })?;
161                Ok::<_, CrpcClientError>(PublicKey::from(key_bytes))
162            })
163            .transpose()?;
164        Ok(GetDataPlaneAddressResponse {
165            address,
166            snap_tun_control_address,
167            snap_static_x25519,
168        })
169    }
170
171    async fn register_snaptun_identity(
172        &self,
173        initiator_identity: PublicKey,
174        psk_share: Option<[u8; 32]>,
175    ) -> Result<Option<[u8; 32]>, CrpcClientError> {
176        let res = self.client.unary_request::<proto::RegisterSnapTunIdentityRequest, proto::RegisterSnapTunIdentityResponse>(
177            &format!("{SERVICE_PATH}{REGISTER_SNAPTUN_IDENTITY}"),
178            &proto::RegisterSnapTunIdentityRequest { initiator_static_x25519: initiator_identity.to_bytes().to_vec(), psk_share: psk_share.unwrap_or([0u8;32]).to_vec() },
179        ).await?;
180        let psk_share = if res.psk_share.as_slice() == [0u8; 32] {
181            None
182        } else {
183            Some(res.psk_share.as_slice().try_into().map_err(
184                |e: std::array::TryFromSliceError| {
185                    CrpcClientError::DecodeError {
186                        context: "psk share is not 32 bytes".into(),
187                        source: Some(e.into()),
188                        body: None,
189                    }
190                },
191            )?)
192        };
193        Ok(psk_share)
194    }
195}
196
197#[async_trait]
198impl SnapTunControlPlaneClient for CrpcSnapControlClient {
199    async fn register_identity(
200        &self,
201        initiator_identity: PublicKey,
202        psk_share: Option<[u8; 32]>,
203    ) -> Result<Option<[u8; 32]>, CrpcClientError> {
204        self.register_snaptun_identity(initiator_identity, psk_share)
205            .await
206    }
207}