Skip to main content

iroh_http_core/endpoint/
bind.rs

1//! `IrohEndpoint::bind` constructor and related helpers.
2//!
3//! Split from `mod.rs` so the facade stays ≤ 200 LoC. All other
4//! `IrohEndpoint` methods live in `mod.rs` (accessors), `lifecycle.rs`
5//! (close / drain / serve wiring), and `observe.rs` (peer info).
6// bind constructs HandleStore directly; endpoint/ is neither mod http nor
7// mod ffi, so this allow is correct here (same rationale as mod.rs lines 20-22).
8#![allow(clippy::disallowed_types)]
9
10use std::{sync::Arc, time::Duration};
11
12use iroh::{
13    address_lookup::{DnsAddressLookup, PkarrPublisher},
14    endpoint::{IdleTimeout, QuicTransportConfig},
15    Endpoint, RelayMode, SecretKey,
16};
17
18use crate::ffi::handles::{HandleStore, StoreConfig};
19use crate::http::transport::pool::ConnectionPool;
20use crate::{ALPN, ALPN_DUPLEX};
21
22use super::{
23    config::NodeOptions, ffi_bridge::FfiBridge, http_runtime::HttpRuntime,
24    session_runtime::SessionRuntime, transport::Transport, EndpointInner, IrohEndpoint,
25};
26
27impl IrohEndpoint {
28    /// Bind an Iroh endpoint with the supplied options.
29    pub async fn bind(opts: NodeOptions) -> Result<Self, crate::CoreError> {
30        // Validate: if networking is disabled, relay_mode should not be explicitly set to a
31        // network-requiring mode.
32        if opts.networking.disabled
33            && opts
34                .networking
35                .relay_mode
36                .as_deref()
37                .is_some_and(|m| !matches!(m, "disabled"))
38        {
39            return Err(crate::CoreError::invalid_input(
40                "networking.disabled is true but relay_mode is set to a non-disabled value; \
41                 set relay_mode to \"disabled\" or omit it when networking.disabled is true",
42            ));
43        }
44
45        let relay_mode = if opts.networking.disabled {
46            RelayMode::Disabled
47        } else {
48            match opts.networking.relay_mode.as_deref() {
49                None | Some("default") => RelayMode::Default,
50                Some("staging") => RelayMode::Staging,
51                Some("disabled") => RelayMode::Disabled,
52                Some("custom") => {
53                    if opts.networking.relays.is_empty() {
54                        return Err(crate::CoreError::invalid_input(
55                            "relay_mode \"custom\" requires at least one URL in `relays`",
56                        ));
57                    }
58                    let urls = opts
59                        .networking
60                        .relays
61                        .iter()
62                        .map(|u| {
63                            u.parse::<iroh::RelayUrl>()
64                                .map_err(crate::CoreError::invalid_input)
65                        })
66                        .collect::<Result<Vec<_>, _>>()?;
67                    RelayMode::custom(urls)
68                }
69                Some(other) => {
70                    return Err(crate::CoreError::invalid_input(format!(
71                        "unknown relay_mode: {other}"
72                    )))
73                }
74            }
75        };
76
77        // Validate capabilities against known ALPN values.
78        for cap in &opts.capabilities {
79            if !crate::KNOWN_ALPNS.contains(&cap.as_str()) {
80                return Err(crate::CoreError::invalid_input(format!(
81                    "unknown ALPN capability: \"{cap}\"; valid values are {:?}",
82                    crate::KNOWN_ALPNS,
83                )));
84            }
85        }
86
87        // Validate compression level range (zstd accepts 1–22).
88        if let Some(level) = opts.compression.as_ref().and_then(|c| c.level) {
89            if !(1..=22).contains(&level) {
90                return Err(crate::CoreError::invalid_input(format!(
91                    "compression level must be 1–22, got {level}"
92                )));
93            }
94        }
95
96        let alpns: Vec<Vec<u8>> = if opts.capabilities.is_empty() {
97            // Advertise both ALPN variants.
98            vec![ALPN_DUPLEX.to_vec(), ALPN.to_vec()]
99        } else {
100            let mut list: Vec<Vec<u8>> = opts
101                .capabilities
102                .iter()
103                .map(|c| c.as_bytes().to_vec())
104                .collect();
105            // Always include the base protocol so the node can talk to base-only peers.
106            if !list.iter().any(|a| a == ALPN) {
107                list.push(ALPN.to_vec());
108            }
109            list
110        };
111
112        let mut builder = Endpoint::builder(iroh::endpoint::presets::Minimal)
113            .relay_mode(relay_mode)
114            .alpns(alpns);
115
116        // DNS discovery (enabled by default unless networking.disabled).
117        if !opts.networking.disabled && opts.discovery.enabled {
118            if let Some(ref url_str) = opts.discovery.dns_server {
119                let url: url::Url = url_str.parse().map_err(|e| {
120                    crate::CoreError::invalid_input(format!("invalid dns_discovery URL: {e}"))
121                })?;
122                builder = builder
123                    .address_lookup(PkarrPublisher::builder(url.clone()))
124                    .address_lookup(DnsAddressLookup::builder(
125                        url.host_str().unwrap_or_default().to_string(),
126                    ));
127            } else {
128                builder = builder
129                    .address_lookup(PkarrPublisher::n0_dns())
130                    .address_lookup(DnsAddressLookup::n0_dns());
131            }
132        }
133
134        if let Some(key_bytes) = opts.key {
135            builder = builder.secret_key(SecretKey::from_bytes(&key_bytes));
136        }
137
138        if let Some(ms) = opts.networking.idle_timeout_ms {
139            let timeout = IdleTimeout::try_from(Duration::from_millis(ms)).map_err(|e| {
140                crate::CoreError::invalid_input(format!("idle_timeout_ms out of range: {e}"))
141            })?;
142            let transport = QuicTransportConfig::builder()
143                .max_idle_timeout(Some(timeout))
144                .max_concurrent_bidi_streams(128u32.into())
145                .build();
146            builder = builder.transport_config(transport);
147        } else {
148            let transport = QuicTransportConfig::builder()
149                .max_concurrent_bidi_streams(128u32.into())
150                .build();
151            builder = builder.transport_config(transport);
152        }
153
154        // Bind address(es).
155        for addr_str in &opts.networking.bind_addrs {
156            let sock: std::net::SocketAddr = addr_str.parse().map_err(|e| {
157                crate::CoreError::invalid_input(format!("invalid bind address \"{addr_str}\": {e}"))
158            })?;
159            builder = builder.bind_addr(sock).map_err(|e| {
160                crate::CoreError::invalid_input(format!("bind address \"{addr_str}\": {e}"))
161            })?;
162        }
163
164        // Proxy configuration.
165        if let Some(ref proxy) = opts.networking.proxy_url {
166            let url: url::Url = proxy
167                .parse()
168                .map_err(|e| crate::CoreError::invalid_input(format!("invalid proxy URL: {e}")))?;
169            builder = builder.proxy_url(url);
170        } else if opts.networking.proxy_from_env {
171            builder = builder.proxy_from_env();
172        }
173
174        if opts.keylog {
175            builder = builder.keylog(true);
176        }
177
178        let ep: Endpoint = builder.bind().await.map_err(classify_bind_error)?;
179        let node_id_str = crate::base32_encode(ep.id().as_bytes());
180
181        let store_config = StoreConfig {
182            channel_capacity: opts
183                .streaming
184                .channel_capacity
185                .unwrap_or(crate::ffi::handles::DEFAULT_CHANNEL_CAPACITY)
186                .max(1),
187            max_chunk_size: opts
188                .streaming
189                .max_chunk_size_bytes
190                .unwrap_or(crate::ffi::handles::DEFAULT_MAX_CHUNK_SIZE)
191                .max(1),
192            drain_timeout: Duration::from_millis(
193                opts.streaming
194                    .drain_timeout_ms
195                    .unwrap_or(crate::ffi::handles::DEFAULT_DRAIN_TIMEOUT_MS),
196            ),
197            max_handles: crate::ffi::handles::DEFAULT_MAX_HANDLES,
198            ttl: Duration::from_millis(
199                opts.streaming
200                    .handle_ttl_ms
201                    .unwrap_or(crate::ffi::handles::DEFAULT_SLAB_TTL_MS),
202            ),
203        };
204        let sweep_ttl = store_config.ttl;
205        let sweep_interval = Duration::from_millis(
206            opts.streaming
207                .sweep_interval_ms
208                .unwrap_or(crate::ffi::handles::DEFAULT_SWEEP_INTERVAL_MS),
209        );
210        let (closed_tx, closed_rx) = tokio::sync::watch::channel(false);
211        let (event_tx, event_rx) =
212            tokio::sync::mpsc::channel::<crate::http::events::TransportEvent>(256);
213
214        let pool = ConnectionPool::new(
215            opts.pool.max_connections,
216            opts.pool
217                .idle_timeout_ms
218                .map(std::time::Duration::from_millis),
219            Some(event_tx.clone()),
220        );
221
222        let max_header_size = match opts.max_header_size {
223            Some(0) => {
224                return Err(crate::CoreError::invalid_input(
225                    "max_header_size must be > 0; use None for the default (65536)",
226                ));
227            }
228            None => 64 * 1024,
229            Some(n) => n,
230        };
231
232        let inner = Arc::new(EndpointInner {
233            transport: Transport { ep, node_id_str },
234            http: HttpRuntime {
235                pool,
236                max_header_size,
237                max_response_body_bytes: opts
238                    .max_response_body_bytes
239                    .unwrap_or(crate::http::server::DEFAULT_MAX_RESPONSE_BODY_BYTES),
240                active_connections: Arc::new(std::sync::atomic::AtomicUsize::new(0)),
241                active_requests: Arc::new(std::sync::atomic::AtomicUsize::new(0)),
242                compression: opts.compression,
243            },
244            session: SessionRuntime {
245                serve_handle: std::sync::Mutex::new(None),
246                serve_stopped_early: std::sync::atomic::AtomicBool::new(false),
247                serve_done_rx: std::sync::Mutex::new(None),
248                closed_tx,
249                closed_rx,
250                event_tx,
251                event_rx: std::sync::Mutex::new(Some(event_rx)),
252                path_subs: dashmap::DashMap::new(),
253            },
254            ffi: FfiBridge {
255                handles: HandleStore::new(store_config),
256            },
257        });
258
259        // Start per-endpoint sweep task (held alive via Weak reference).
260        if !sweep_ttl.is_zero() {
261            let weak = Arc::downgrade(&inner);
262            tokio::spawn(async move {
263                let mut ticker = tokio::time::interval(sweep_interval);
264                loop {
265                    ticker.tick().await;
266                    let Some(inner) = weak.upgrade() else {
267                        break;
268                    };
269                    inner.ffi.handles.sweep(sweep_ttl);
270                    drop(inner); // release strong ref between ticks
271                }
272            });
273        }
274
275        Ok(Self { inner })
276    }
277}
278
279/// Classify a bind error into a `CoreError`.
280pub(super) fn classify_bind_error(e: impl std::fmt::Display) -> crate::CoreError {
281    let msg = e.to_string();
282    crate::CoreError::connection_failed(msg)
283}
284
285/// Parse an optional list of socket address strings into `SocketAddr` values.
286///
287/// Returns `Err` if any string cannot be parsed as a `host:port` address so
288/// that callers can surface misconfiguration rather than silently ignoring it.
289pub fn parse_direct_addrs(
290    addrs: &Option<Vec<String>>,
291) -> Result<Option<Vec<std::net::SocketAddr>>, String> {
292    match addrs {
293        None => Ok(None),
294        Some(v) => {
295            let mut out = Vec::with_capacity(v.len());
296            for s in v {
297                let addr = s
298                    .parse::<std::net::SocketAddr>()
299                    .map_err(|e| format!("invalid direct address {s:?}: {e}"))?;
300                out.push(addr);
301            }
302            Ok(Some(out))
303        }
304    }
305}