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 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 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 ensure!(
102 matches!(rcan.capability_origin(), CapabilityOrigin::Issuer),
103 "invalid capability origin: expected first-party token"
104 );
105
106 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 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 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 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 client.rpc(Auth { caps: rcan }).await.unwrap();
186
187 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 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 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}