Skip to main content

iroh_services/
client_host.rs

1use anyhow::{Result, ensure};
2use iroh::{
3    Endpoint, EndpointId,
4    endpoint::Connection,
5    protocol::{AcceptError, ProtocolHandler},
6};
7use irpc::WithChannels;
8use irpc_iroh::read_request;
9use n0_error::AnyError;
10use rcan::{Capability, CapabilityOrigin, Rcan};
11use tracing::{debug, warn};
12
13use crate::{
14    caps::{Caps, NetDiagnosticsCap},
15    protocol::{ClientHostProtocol, NetDiagnosticsMessage, RemoteError},
16};
17
18/// The ALPN for sending messages from the cloud node to the client.
19pub const CLIENT_HOST_ALPN: &[u8] = b"n0/n0des-client-host/1";
20
21pub type ClientHostClient = irpc::Client<ClientHostProtocol>;
22
23/// Protocol handler for cloud-to-endpoint connections.
24#[derive(Debug)]
25pub struct ClientHost {
26    endpoint: Endpoint,
27}
28
29impl ProtocolHandler for ClientHost {
30    async fn accept(&self, connection: Connection) -> Result<(), AcceptError> {
31        self.handle_connection(connection).await.map_err(|e| {
32            let boxed: Box<dyn std::error::Error + Send + Sync> = e.into();
33            AcceptError::from(AnyError::from(boxed))
34        })
35    }
36}
37
38impl ClientHost {
39    pub fn new(endpoint: &Endpoint) -> Self {
40        Self {
41            endpoint: endpoint.clone(),
42        }
43    }
44
45    async fn handle_connection(&self, connection: Connection) -> Result<()> {
46        let remote_node_id = connection.remote_id();
47        let Some(first_request) = read_request::<ClientHostProtocol>(&connection).await? else {
48            return Ok(());
49        };
50
51        let NetDiagnosticsMessage::Auth(WithChannels { inner, tx, .. }) = first_request else {
52            debug!(remote_node_id = %remote_node_id.fmt_short(), "Expected initial auth message");
53            connection.close(400u32.into(), b"Expected initial auth message");
54            return Ok(());
55        };
56        let rcan = inner.caps;
57        let capability = rcan.capability();
58
59        let res = verify_rcan(&self.endpoint, remote_node_id, &rcan);
60        match res {
61            Ok(()) => tx.send(()).await?,
62            Err(err) => {
63                warn!("authentication failed: {err:?}");
64                connection.close(401u32.into(), b"Unauthorized");
65                return Ok(());
66            }
67        }
68
69        // Read exactly one RunNetworkDiagnostics request
70        let Some(request) = read_request::<ClientHostProtocol>(&connection).await? else {
71            return Ok(());
72        };
73
74        match request {
75            NetDiagnosticsMessage::Auth(_) => {
76                connection.close(400u32.into(), b"Unexpected auth message");
77                anyhow::bail!("unexpected auth message");
78            }
79            NetDiagnosticsMessage::RunNetworkDiagnostics(msg) => {
80                let WithChannels { tx, .. } = msg;
81                let needed_caps = Caps::new([NetDiagnosticsCap::GetAny]);
82                if !capability.permits(&needed_caps) {
83                    return send_missing_caps(tx, needed_caps).await;
84                }
85
86                let report =
87                    crate::net_diagnostics::checks::run_diagnostics(&self.endpoint).await?;
88                tx.send(Ok(report))
89                    .await
90                    .inspect_err(|e| warn!("sending network diagnostics response: {:?}", e))?;
91            }
92        }
93
94        connection.closed().await;
95        Ok(())
96    }
97}
98
99fn verify_rcan(endpoint: &Endpoint, remote_node: EndpointId, rcan: &Rcan<Caps>) -> Result<()> {
100    // Must be a first-party token (not delegated)
101    ensure!(
102        matches!(rcan.capability_origin(), CapabilityOrigin::Issuer),
103        "invalid capability origin: expected first-party token"
104    );
105
106    // Issuer must be this endpoint (we issued this grant)
107    ensure!(
108        EndpointId::try_from(rcan.issuer().as_bytes())
109            .map(|id| id == endpoint.id())
110            .unwrap_or(false),
111        "invalid issuer: RCAN was not issued by this endpoint"
112    );
113
114    // Audience must be the remote node (the token is for them)
115    ensure!(
116        EndpointId::try_from(rcan.audience().as_bytes())
117            .map(|id| id == remote_node)
118            .unwrap_or(false),
119        "invalid audience: RCAN audience does not match remote node"
120    );
121
122    Ok(())
123}
124
125async fn send_missing_caps<T>(
126    tx: irpc::channel::oneshot::Sender<Result<T, RemoteError>>,
127    missing_caps: Caps,
128) -> Result<()> {
129    tx.send(Err(RemoteError::MissingCapability(missing_caps)))
130        .await?;
131    Ok(())
132}
133
134#[cfg(test)]
135mod tests {
136    use iroh::{address_lookup::MemoryLookup, protocol::Router};
137    use irpc_iroh::IrohLazyRemoteConnection;
138    use n0_future::time::Duration;
139
140    use super::*;
141    use crate::{
142        ALPN,
143        caps::create_grant_token,
144        protocol::{Auth, IrohServicesClient, RunNetworkDiagnostics},
145    };
146
147    #[tokio::test]
148    async fn test_diagnostics_host_run_diagnostics() {
149        let lookup = MemoryLookup::new();
150        let server_ep = iroh::Endpoint::empty_builder()
151            .address_lookup(lookup.clone())
152            .bind()
153            .await
154            .unwrap();
155
156        let client_ep = iroh::Endpoint::empty_builder()
157            .address_lookup(lookup.clone())
158            .bind()
159            .await
160            .unwrap();
161
162        let host = ClientHost::new(&server_ep);
163        let router = Router::builder(server_ep.clone())
164            .accept(CLIENT_HOST_ALPN, host)
165            .spawn();
166
167        // The server grants capabilities to the client.
168        let rcan = create_grant_token(
169            server_ep.secret_key().clone(),
170            client_ep.id(),
171            Duration::from_secs(3600),
172            Caps::for_shared_secret(),
173        )
174        .unwrap();
175
176        // Connect on the net diagnostics ALPN
177        let conn = IrohLazyRemoteConnection::new(
178            client_ep.clone(),
179            server_ep.addr(),
180            CLIENT_HOST_ALPN.to_vec(),
181        );
182        let client = ClientHostClient::boxed(conn);
183
184        // authenticate with the server-issued grant
185        client.rpc(Auth { caps: rcan }).await.unwrap();
186
187        // send RunNetworkDiagnostics and verify we get a report back
188        let result = client.rpc(RunNetworkDiagnostics).await.unwrap();
189        let report = result.expect("expected Ok(DiagnosticsReport)");
190        assert_eq!(report.endpoint_id, server_ep.id());
191
192        router.shutdown().await.unwrap();
193        client_ep.close().await;
194    }
195
196    #[tokio::test]
197    async fn test_client_host_rejects_self_signed_rcan() {
198        let lookup = MemoryLookup::new();
199        let server_ep = iroh::Endpoint::empty_builder()
200            .address_lookup(lookup.clone())
201            .bind()
202            .await
203            .unwrap();
204
205        let client_ep = iroh::Endpoint::empty_builder()
206            .address_lookup(lookup.clone())
207            .bind()
208            .await
209            .unwrap();
210
211        let host = ClientHost::new(&server_ep);
212        let router = Router::builder(server_ep.clone())
213            .accept(ALPN, host)
214            .spawn();
215
216        // Client creates its own RCAN (self-signed, not issued by server).
217        let rcan = create_grant_token(
218            client_ep.secret_key().clone(),
219            client_ep.id(),
220            Duration::from_secs(3600),
221            Caps::for_shared_secret(),
222        )
223        .unwrap();
224
225        let conn =
226            IrohLazyRemoteConnection::new(client_ep.clone(), server_ep.addr(), ALPN.to_vec());
227        let client = IrohServicesClient::boxed(conn);
228
229        // auth should fail because the RCAN issuer is the client, not the server
230        let result = client.rpc(Auth { caps: rcan }).await;
231        assert!(
232            result.is_err(),
233            "expected auth to be rejected for self-signed RCAN"
234        );
235
236        router.shutdown().await.unwrap();
237        client_ep.close().await;
238    }
239}