Skip to main content

iroh_services/
protocol.rs

1use std::collections::BTreeMap;
2
3use anyhow::Result;
4use irpc::{channel::oneshot, rpc_requests};
5use rcan::Rcan;
6use serde::{Deserialize, Serialize};
7use uuid::Uuid;
8
9use crate::{caps::Caps, net_diagnostics::DiagnosticsReport};
10
11/// The main ALPN for connecting from the client to the cloud node.
12///
13/// # Versioning
14///
15/// The wire protocol is append-only and does not bump this ALPN for additive
16/// changes. postcard encodes enum variants by index, so as long as new
17/// [`IrohServicesProtocol`] and [`RemoteError`] variants are only appended
18/// (never inserted, reordered, or removed), older messages stay wire-compatible:
19///
20/// - An older client always works against a newer server: the server decodes
21///   every request the client can send, and only replies with error variants the
22///   client already knows.
23/// - A newer client against an older server keeps working for the pre-existing
24///   requests (auth, metrics, and so on); a request the old server does not know
25///   fails as a per-request error rather than breaking the connection.
26///
27/// The cloud node is deployed at or ahead of the clients that talk to it, so the
28/// second case is transient and limited to the new calls. A breaking change
29/// (reordering or removing variants, or changing a message's shape) requires a
30/// new ALPN.
31pub const ALPN: &[u8] = b"/iroh/n0des/1";
32
33pub type IrohServicesClient = irpc::Client<IrohServicesProtocol>;
34
35/// New request variants MUST be appended, never inserted or reordered. See the
36/// versioning policy on [`ALPN`].
37#[rpc_requests(message = ServicesMessage)]
38#[derive(Debug, Serialize, Deserialize)]
39#[allow(clippy::large_enum_variant)]
40#[non_exhaustive]
41pub enum IrohServicesProtocol {
42    #[rpc(tx=oneshot::Sender<()>)]
43    Auth(Auth),
44    #[rpc(tx=oneshot::Sender<RemoteResult<()>>)]
45    PutMetrics(PutMetrics),
46    #[rpc(tx=oneshot::Sender<Pong>)]
47    Ping(Ping),
48
49    #[rpc(tx=oneshot::Sender<RemoteResult<()>>)]
50    PutNetworkDiagnostics(PutNetworkDiagnostics),
51
52    #[rpc(tx=oneshot::Sender<RemoteResult<()>>)]
53    GrantCap(GrantCap),
54
55    #[rpc(tx=oneshot::Sender<RemoteResult<()>>)]
56    NameEndpoint(NameEndpoint),
57
58    #[rpc(tx=oneshot::Sender<RemoteResult<()>>)]
59    SetGroup(SetGroup),
60
61    #[rpc(tx=oneshot::Sender<RemoteResult<()>>)]
62    SetAttributes(SetAttributes),
63}
64
65/// Dedicated protocol for cloud-to-endpoint net diagnostics connections.
66#[rpc_requests(message = NetDiagnosticsMessage)]
67#[derive(Debug, Serialize, Deserialize)]
68#[allow(clippy::large_enum_variant)]
69pub enum ClientHostProtocol {
70    #[rpc(tx=oneshot::Sender<()>)]
71    Auth(Auth),
72    #[rpc(tx=oneshot::Sender<RemoteResult<DiagnosticsReport>>)]
73    RunNetworkDiagnostics(RunNetworkDiagnostics),
74}
75
76pub type RemoteResult<T> = Result<T, RemoteError>;
77
78#[derive(Clone, Serialize, Deserialize, thiserror::Error, Debug)]
79#[non_exhaustive]
80pub enum RemoteError {
81    // The first three variants and their order are the v1 wire contract: postcard
82    // encodes enum variants by index, so a v1 client only decodes these and at
83    // their original positions. New variants MUST be appended after them, and the
84    // server must only send new variants in response to new (v2+) requests.
85    #[error("Missing capability: {}", _0.to_strings().join(", "))]
86    MissingCapability(Caps),
87    #[error("Unauthorized: {}", _0)]
88    AuthError(String),
89    #[error("Internal server error")]
90    InternalServerError,
91    #[error("Invalid input: {}", _0)]
92    InvalidInput(String),
93    #[error("Rate limit exceeded")]
94    RateLimited,
95}
96
97/// Authentication on first request
98#[derive(Debug, Serialize, Deserialize)]
99pub struct Auth {
100    pub caps: Rcan<Caps>,
101}
102
103/// Request to store the given metrics data
104#[derive(Debug, Serialize, Deserialize)]
105pub struct PutMetrics {
106    pub session_id: Uuid,
107    pub update: iroh_metrics::encoding::Update,
108}
109
110/// Simple ping requests
111#[derive(Debug, Serialize, Deserialize)]
112pub struct Ping {
113    pub req_id: [u8; 16],
114}
115
116/// Simple ping response
117#[derive(Debug, Serialize, Deserialize)]
118pub struct Pong {
119    pub req_id: [u8; 16],
120}
121
122/// Publishing network diagnostics
123#[derive(Debug, Serialize, Deserialize)]
124pub struct PutNetworkDiagnostics {
125    pub report: crate::net_diagnostics::DiagnosticsReport,
126}
127
128/// ask this node to run diagnostics & return the result.
129/// present even without the net_diagnostics feature flag because the request
130/// struct is empty in both cases
131#[derive(Debug, Serialize, Deserialize)]
132pub struct RunNetworkDiagnostics;
133
134/// Grant a capability token to the remote endpoint. The remote should store
135/// the RCAN and use it when dialing back to authorize its requests.
136#[derive(Debug, Serialize, Deserialize)]
137pub struct GrantCap {
138    pub cap: Rcan<Caps>,
139}
140
141/// Label the client endpoint cloud-side with a string identifier.
142#[derive(Debug, Serialize, Deserialize)]
143pub struct NameEndpoint {
144    pub name: String,
145}
146
147/// Attach the client endpoint to a single named group cloud-side.
148#[derive(Debug, Serialize, Deserialize)]
149pub struct SetGroup {
150    pub group: String,
151}
152
153/// Replace the arbitrary key-value attributes on the client endpoint cloud-side.
154#[derive(Debug, Serialize, Deserialize)]
155pub struct SetAttributes {
156    pub attributes: BTreeMap<String, String>,
157}
158
159#[cfg(test)]
160mod tests {
161    use std::collections::BTreeMap;
162
163    use super::{RemoteError, SetAttributes, SetGroup};
164    use crate::client::CLIENT_ATTRIBUTE_VALUE_MAX_LENGTH;
165
166    #[test]
167    fn test_remote_error_wire_compat() {
168        // postcard encodes enum variants by their index. v1 clients only know
169        // the first three RemoteError variants, so these indices are a frozen
170        // wire contract; new variants must be appended after them.
171        let idx = |e: &RemoteError| postcard::to_stdvec(e).expect("encode")[0];
172        assert_eq!(idx(&RemoteError::AuthError(String::new())), 1);
173        assert_eq!(idx(&RemoteError::InternalServerError), 2);
174        // v2+ variants, appended after the v1 set.
175        assert_eq!(idx(&RemoteError::InvalidInput(String::new())), 3);
176        assert_eq!(idx(&RemoteError::RateLimited), 4);
177    }
178
179    // The wire format used by irpc (and elsewhere in this crate, see
180    // `api_secret.rs`) is postcard. These round-trips pin the on-the-wire
181    // contract these messages share with the server.
182
183    #[test]
184    fn test_set_group_serde_roundtrip() {
185        // a normal group, plus a unicode group for good measure
186        for group in ["staging", "my-group 👋"] {
187            let msg = SetGroup {
188                group: group.to_string(),
189            };
190            let bytes = postcard::to_stdvec(&msg).expect("postcard serialize");
191            let decoded: SetGroup = postcard::from_bytes(&bytes).expect("postcard deserialize");
192            assert_eq!(decoded.group, msg.group);
193        }
194    }
195
196    #[test]
197    fn test_set_attributes_serde_roundtrip() {
198        // empty map: the documented "clear" case
199        let empty = SetAttributes {
200            attributes: BTreeMap::new(),
201        };
202        let bytes = postcard::to_stdvec(&empty).expect("postcard serialize");
203        let decoded: SetAttributes = postcard::from_bytes(&bytes).expect("postcard deserialize");
204        assert!(decoded.attributes.is_empty());
205        assert_eq!(decoded.attributes, empty.attributes);
206
207        // unicode key/value plus a value at exactly the documented max length
208        let mut attributes = BTreeMap::new();
209        attributes.insert("région 🌍".to_string(), "us-wëst 🚀".to_string());
210        let max_value = "x".repeat(CLIENT_ATTRIBUTE_VALUE_MAX_LENGTH);
211        assert_eq!(max_value.len(), CLIENT_ATTRIBUTE_VALUE_MAX_LENGTH);
212        attributes.insert("max".to_string(), max_value);
213
214        let msg = SetAttributes { attributes };
215        let bytes = postcard::to_stdvec(&msg).expect("postcard serialize");
216        let decoded: SetAttributes = postcard::from_bytes(&bytes).expect("postcard deserialize");
217        assert_eq!(decoded.attributes, msg.attributes);
218    }
219}