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