1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
#[cfg(doc)]
use crate::{
Node,
protocols::{self, Handshake, Reading, Writing},
};
/// The node's configuration. See the source of [`Config::default`] for the defaults.
#[derive(Debug, Clone)]
pub struct Config {
/// A user-friendly identifier of the node. It is visible in the logs, where it allows nodes to be
/// distinguished more easily if multiple are run at the same time.
///
/// note: If set to `None`, the node will automatically be assigned a sequential, zero-based numeric identifier.
pub name: Option<String>,
/// The socket address the node's connection listener should bind to.
///
/// note: If set to `None`, the node will not listen for inbound connections at all.
///
/// note: This is a configuration directive, not a live status. If set to `127.0.0.1:0`,
/// the actual bound port will differ. Always use [`Node::listening_addr`] to retrieve the
/// active runtime address.
///
/// note: Binding to a wildcard address (e.g. `0.0.0.0`) means the library cannot reliably
/// detect a self-connect attempt to one of the host's other local addresses. See the note
/// on [`Node::connect`] for details.
pub listener_addr: Option<SocketAddr>,
/// The depth of the OS accept queue (the `backlog` argument to `listen(2)`): how many
/// fully-established inbound connections the kernel will hold awaiting `accept` before it
/// stops admitting new ones.
///
/// note: This value is silently capped by the OS to `net.core.somaxconn` on Linux
/// (`kern.ipc.somaxconn` on macOS/BSD). The effective depth is `min(listener_backlog, somaxconn)`,
/// so raising it past the system limit does nothing until that limit is raised as well.
///
/// note: When the queue is full the kernel does not, by default, actively refuse the connection -
/// it silently drops the handshake completion and the peer retransmits on the TCP timer, so the
/// symptom is a connection that appears to stall for seconds rather than one that fails fast.
/// (On Linux, `net.ipv4.tcp_abort_on_overflow=1` turns these into immediate resets instead.)
///
/// note: For graceful backpressure under a burst, keep this at least on the order of
/// [`Config::max_connecting`]: inbound setup is throttled to `max_connecting` at a time, and the
/// backlog is what holds the surplus while that pipeline drains. A backlog much smaller than
/// `max_connecting` lets bursts overflow before setup catches up.
pub listener_backlog: u32,
/// Sets `SO_REUSEPORT` on the listener socket, permitting several *live* listeners to bind the
/// node's address at the same time, with the kernel load-balancing inbound connections across
/// them. Defaults to `false`.
///
/// note: This is more permissive than the `SO_REUSEADDR` the node always sets - that one only
/// allows *rebinding* a port left in `TIME_WAIT` by a closed socket, not two simultaneously
/// active listeners on one port.
///
/// note: For a lone listener that never shares its port, this option does nothing; it earns its
/// keep only once a second socket joins the address. The two motivating cases are running
/// multiple nodes or processes on one port as kernel-balanced shards, and zero-downtime restarts,
/// where a successor binds the port and begins accepting before its predecessor is retired, so
/// the address is never momentarily unavailable.
///
/// note: Even, hash-based balancing across the group is a Linux feature (3.9+); other platforms'
/// semantics differ, and where `SO_REUSEPORT` is unavailable [`Node::toggle_listener`] returns an
/// error when this is set. Enabling it also makes the port *joinable*: any socket with the same
/// effective UID can bind it and siphon off a share of inbound connections.
///
/// note: When used to shard, the connection limits ([`Config::max_connections`],
/// [`Config::max_connecting`], and especially [`Config::max_connections_per_ip`]) apply *per
/// listener*, not across the group, since each node tracks its connections independently.
pub reuse_listener_port: bool,
/// The maximum number of active connections the node can maintain at any given time.
///
/// note: For accuracy and performance, the pending connections are also included when checking
/// this limit - it is assumed that they may all conclude successfully.
///
/// note: As a `u16`, a single node tops out at 65,535 connections; scaling beyond that means
/// distributing load across multiple nodes/listeners.
///
/// note: Must not be `0` - [`Node::new`] panics on such a value. A node that should never
/// have any connections can simply not listen and not dial instead.
pub max_connections: u16,
/// The maximum number of active connections the node can maintain with a single IP.
///
/// note: Like [`Config::max_connections`], pending connections are also included in the
/// related calculations.
///
/// note: This limit matches the exact IP address. It does not aggregate subnets. An attacker
/// with an IPv6 `/64` subnet can bypass this limit by assigning a unique address for every
/// connection (up to [`Config::max_connections`]). If you expose your node to the public IPv6
/// internet, rely on the global connection limit for resource protection, or implement an
/// application-level subnet filter in [`Handshake`].
///
/// note: Conversely, when many connections legitimately share one source IP - load tests, a NAT
/// gateway, a reverse proxy, or anything over loopback - raise this to match, or those peers are
/// rejected once the per-IP count is hit. Note the default outside the `test` feature is 1.
///
/// note: Must not be `0` - [`Node::new`] panics on such a value.
pub max_connections_per_ip: u16,
/// The maximum number of simultaneous connection attempts (a.k.a. pending connections), covering
/// both outbound connects in progress and inbound connections still being accepted and handshaked.
///
/// note: It should not be greater than [`Config::max_connections`]: pending connections count
/// towards that limit, so a value above it can never actually be reached.
///
/// note: On the inbound path this doubles as a backpressure bound - at most `max_connecting`
/// inbound connections are set up concurrently, and any surplus waits in the OS accept queue
/// (bar a single already-accepted connection awaiting a free setup slot; see
/// [`Config::listener_backlog`], which should be sized accordingly).
///
/// note: The shared budget is a single hard ceiling; an inbound flood that would starve outbound
/// would *already* be stalling all new inbound peers (backpressured into the OS accept queue),
/// so it is a node-health event the operator
/// must detect and handle. Mitigation is left to the application: shed inbound load via
/// [`Node::toggle_listener`], reject unwanted peers early in [`Handshake`], or block offending
/// address ranges at the network layer. See [`Node::connect`] for detecting and reacting to this
/// condition.
///
/// note: Must not be `0` - [`Node::new`] panics on such a value. To stop admitting inbound
/// connections at runtime, use [`Node::toggle_listener`] instead.
pub max_connecting: u16,
/// The maximum time (in milliseconds) allowed to establish a raw (before the [`Handshake`] protocol) TCP connection.
///
/// note: Unlike [`Reading::IDLE_TIMEOUT_MS`], a value of `0` does not disable the timeout -
/// it fails every connection attempt (almost) instantly.
pub connection_timeout_ms: u16,
}
impl Default for Config {
fn default() -> Self {
Self {
name: None,
#[cfg(not(feature = "test"))]
listener_addr: Some((IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0).into()),
#[cfg(feature = "test")]
listener_addr: Some((IpAddr::V4(Ipv4Addr::LOCALHOST), 0).into()),
listener_backlog: 128,
reuse_listener_port: false,
max_connections: 100,
#[cfg(not(feature = "test"))]
max_connections_per_ip: 1,
#[cfg(feature = "test")]
max_connections_per_ip: 100,
max_connecting: 100,
connection_timeout_ms: 1_000,
}
}
}