How to Query Data from the iroh Network: Endpoint, Streams, and ALPN Protocols
To query data from the iroh network, create an Endpoint using the builder pattern, connect to a remote peer via its EndpointId and an agreed-upon ALPN protocol string, open a bidirectional QUIC stream, send your query payload, and read the response bytes.
Querying data from the iroh network leverages a peer-to-peer, QUIC-based transport layer that operates without centralized servers. The n0-computer/iroh repository provides Rust APIs that abstract NAT traversal, relay fallback, and TLS encryption. This guide walks through the exact sequence of method calls—using Endpoint::bind, Endpoint::connect, and bidirectional streams—to execute remote queries against the iroh network.
Initializing the Endpoint
The Endpoint is the root object that owns all networking resources, including sockets, TLS configuration, and address-lookup services. In iroh/src/endpoint.rs, the Builder struct configures these components before binding to an address.
Binding with Default Presets
Use Endpoint::bind with presets::N0 to create a production-ready endpoint with relay support and DNS discovery enabled.
use iroh::{Endpoint, endpoint::presets};
let endpoint = Endpoint::bind(presets::N0).await?;
This invokes Builder::bind (lines 224–260 in iroh/src/endpoint.rs), which initializes the underlying QUIC socket, configures the TlsConfig, and sets up the address-lookup service.
Establishing Connections with ALPN
To query data from the iroh network, you must connect to a specific peer using its EndpointId and negotiate an Application-Layer Protocol Negotiation (ALPN) string.
The connect Method
The Endpoint::connect method (implemented at lines 250–270 in iroh/src/endpoint.rs) resolves the remote address via DNS or relay servers, performs the QUIC handshake, and returns a Connection.
const ALPN: &[u8] = b"iroh-example/query/0";
let conn = endpoint.connect(remote_id, ALPN).await?;
Both peers must present matching ALPN strings to accept the connection. The method internally calls connect_with_opts to handle transport configuration, relay selection, and TLS setup.
Opening Streams and Sending Queries
QUIC connections support multiplexed streams. For request-response patterns, open a bidirectional stream.
Bidirectional Stream Operations
Call conn.open_bi().await? to create a (SendStream, RecvStream) pair. Write your query payload using write_all, then signal end-of-data with finish():
let (mut send, mut recv) = conn.open_bi().await?;
let query = b"search term";
send.write_all(query).await?;
send.finish()?;
Receiving and Processing Responses
Read the response bytes from the receive stream. In the examples/search.rs implementation, the remote returns a u64 count representing the number of matches:
let mut buf = [0u8; 8];
recv.read_exact(&mut buf).await?;
let answer = u64::from_le_bytes(buf);
Implementing the Server Side
To accept queries, implement the ProtocolHandler trait from iroh/src/protocol.rs and register it with a Router.
use iroh::protocol::{ProtocolHandler, Router};
use std::sync::Arc;
struct QueryHandler;
#[async_trait::async_trait]
impl ProtocolHandler for QueryHandler {
async fn accept(&self, conn: iroh::connection::Connection) -> Result<(), iroh::protocol::AcceptError> {
let (mut send, mut recv) = conn.accept_bi().await?;
let data = recv.read_to_end(1024).await?;
// Process query and respond
let result = data.len() as u64;
send.write_all(&result.to_le_bytes()).await?;
send.finish()?;
conn.closed().await;
Ok(())
}
}
// Register the handler
let router = Router::builder(endpoint.clone())
.accept(ALPN.to_vec(), Arc::new(QueryHandler))
.spawn()
.await?;
Complete Working Example
Below is a self-contained example demonstrating both client and server sides:
use iroh::{Endpoint, endpoint::presets, EndpointId};
use iroh::protocol::{ProtocolHandler, Router};
use n0_error::Result;
use tokio::io::AsyncWriteExt;
use std::sync::Arc;
const ALPN: &[u8] = b"iroh-example/text-search/0";
#[tokio::main]
async fn main() -> Result<()> {
// Initialize endpoint
let endpoint = Endpoint::bind(presets::N0).await?;
// Setup protocol handler
let router = Router::builder(endpoint.clone())
.accept(ALPN.to_vec(), Arc::new(EchoHandler))
.spawn().await?;
// Client side: connect and query
let remote_id: EndpointId = /* obtain from peer */;
let conn = endpoint.connect(remote_id, ALPN).await?;
let (mut send, mut recv) = conn.open_bi().await?;
send.write_all(b"hello world").await?;
send.finish()?;
let mut buf = [0u8; 8];
recv.read_exact(&mut buf).await?;
println!("Response: {}", u64::from_le_bytes(buf));
router.shutdown().await?;
Ok(())
}
struct EchoHandler;
#[async_trait::async_trait]
impl ProtocolHandler for EchoHandler {
async fn accept(&self, conn: iroh::connection::Connection) -> Result<(), iroh::protocol::AcceptError> {
let (mut send, mut recv) = conn.accept_bi().await?;
let data = recv.read_to_end(1024).await?;
send.write_all(&(data.len() as u64).to_le_bytes()).await?;
send.finish()?;
conn.closed().await;
Ok(())
}
}
Key Source Files and Components
Understanding these core files helps when debugging or extending query functionality:
iroh/src/endpoint.rs: ContainsEndpoint,Builder, and theconnectmethod (lines 250–270) that manages peer resolution and QUIC handshakes.iroh/src/protocol.rs: DefinesProtocolHandlerandRouterfor registering application-level protocols.examples/search.rs: Complete implementation of a text-search protocol demonstrating both query client and responder patterns.iroh/src/connection.rs: ProvidesConnection::open_biandConnection::accept_bifor stream management.
Summary
- Create an Endpoint using
Endpoint::bind(presets::N0)to initialize networking resources. - Dial remote peers with
Endpoint::connect(remote_id, ALPN), which handles NAT traversal via relays and address lookup. - Open bidirectional streams using
conn.open_bi()to send queries and receive responses. - Signal EOF with
send.finish()to indicate the query payload is complete. - Implement ProtocolHandler to accept incoming queries on the server side using
Router::builder.
Frequently Asked Questions
What is the role of ALPN when querying data from the iroh network?
The ALPN (Application-Layer Protocol Negotiation) string identifies the specific protocol version both peers expect to speak. When you call Endpoint::connect, the iroh library attempts to negotiate this protocol with the remote endpoint. If the remote peer has not registered a handler for that ALPN via Router::accept, the connection fails. This allows multiple protocols to coexist on the same endpoint.
How does iroh resolve EndpointId addresses behind NAT?
The Endpoint::connect method in iroh/src/endpoint.rs invokes the address-lookup module to resolve an EndpointId into reachable addresses. It attempts direct connections first, then falls back to configured relay servers (specified in RelayMode during the build phase) to traverse NAT devices. This process is transparent to the application code.
Can I use unidirectional streams instead of bidirectional for queries?
Yes. While open_bi() creates a bidirectional stream suitable for request-response patterns, you can use open_uni() for one-way data transmission. However, for standard query-response workflows, bidirectional streams are recommended because they keep the request and response logically paired on the same QUIC stream ID.
How do I handle large data responses when querying the iroh network?
For large payloads, the remote endpoint should return content hashes (using the iroh-blobs crate) rather than raw bytes. The client can then fetch these blobs using the content-addressing system. This approach avoids blocking the QUIC stream with large transfers and allows for parallel downloads and verification.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →