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