-
Notifications
You must be signed in to change notification settings - Fork 4
Configuration Reference
Options are scoped the way Netty's are: options configure the thing you
created, child_options configure each session a listener accepts. A client has
no children, so its options are the session's.
void ConfigureServer() {
// no ConnectionType, so this is ZDT, the default
ServerConfig config{"0.0.0.0", 25000, std::chrono::seconds(10)};
// options: the listener itself
config.options.max_connections = 4096;
// child_options: every session the listener accepts
config.child_options.common.idle_timeout = std::chrono::seconds(30);
config.child_options.common.encryption = true;
config.child_options.common.compression = CompressionType::Zstandard;
config.child_options.common.compression_threshold = 128;
config.child_options.common.send_queue_capacity = 1024;
config.child_options.tcp.no_delay = true;
config.child_options.zdt.max_datagrams_in_flight = 256;
config.child_options.zdt.rto_min = std::chrono::milliseconds(50);
config.child_options.zdt.mtu_ladder.Set({1492, 1200, 576});
}
void ConfigureClient() {
// a client has no children, so its options are the session's
ClientConfig config{"127.0.0.1", 25000, std::chrono::seconds(10)};
config.options.common.idle_timeout = std::chrono::seconds(30);
config.options.common.keepalive_interval = std::chrono::milliseconds(500);
}The tcp and zdt groups are always present and always settable. Only the one
matching the session's connection_type is read, and setting the other is
neither an error nor a warning, so a zdt block on a TCP session is dead
configuration that looks live.
They are plain structs, not a typed-key map: every option is known at compile time, so unset fields simply keep their defaults.
| Field | Notes |
|---|---|
server_ip / server_port
|
Where to connect (client) or bind (server) |
connection_timeout |
Give up on a connection attempt after this. Zero waits forever |
connection_type |
ConnectionType::ZDT (the default) or ConnectionType::TCP
|
options |
Listener scope on a server; session scope on a client |
child_options |
Server only: applied to each accepted session |
The address string also accepts a Unix domain socket path, spelled
unix:/run/app.sock. That requires ConnectionType::TCP (it is the stream
backend; ZDT refuses paths), the port is ignored, and it is POSIX only. The
server takes over a stale socket file on bind and unlinks it on close.
Applies to any session, whatever the transport.
| Option | Default | Notes |
|---|---|---|
idle_timeout |
10 s | Drop a session that has heard nothing this long. Both transports implement it. Zero disables |
keepalive_interval |
1000 ms | Ping a connection with nothing else to send, keeping it inside the peer's idle_timeout. Transport-internal; never reaches the application. Zero disables |
collect_metrics |
true |
Ignored unless built with ZNET_ENABLE_METRICS
|
encryption |
true |
Read only on the accepting side. See Encryption and Compression |
compression |
Default |
Resolved at session start to whatever the build supports |
compression_threshold |
128 B | Below this, messages go uncompressed |
send_queue_capacity |
512 | Packets a session holds for its worker to encode |
dump_on_decode_failure |
false |
Log a hex dump of a payload whose frame fails to decode, capped at 512 bytes |
max_invalid_frames |
16 | Close a session once this many of its frames failed to decode. Zero disables |
A frame that fails to decode costs the rest of its buffer, since the framing
after it cannot be trusted. This threshold is what stops a peer from making
that a free, repeatable attack. It counts over the session's whole life:
unreadable headers, declared lengths the buffer cannot back, serializers
refusing a frame or reading past their frame. Unknown packet ids are not
counted, since they skip cleanly and can be honest version skew. The count is
visible as invalid_frames in Metrics and via
session->invalid_frames().
dump_on_decode_failure logs the evidence when one happens: the offset of the
failing frame and the payload bytes. Off by default because payloads are user
data and this puts them in the log.
This is the backpressure knob. SendPacket returns false once the queue is
full, which is how an application learns it is outrunning the link. A refusal
loses nothing, since you still hold the packet.
The queue is a ring allocated whole at construction, roughly 32 bytes a slot rounded up to a power of two, with no per-message allocation afterwards. Size it to the largest burst worth absorbing between two of the worker's ticks, not to the total you ever expect to send. A transport queues further messages behind its own congestion window, so this is not the whole picture.
Read only when connection_type is ConnectionType::TCP. Ignored on a ZDT
session.
| Option | Default | Notes |
|---|---|---|
no_delay |
true |
Disables Nagle. Leave it on unless you are bulk-transferring |
reuse_address |
true |
SO_REUSEADDR, so a restart can rebind a port in TIME_WAIT
|
send_buffer_size |
0 | Socket send buffer. Zero keeps the OS default |
receive_buffer_size |
0 | Socket receive buffer. Zero keeps the OS default |
Read only when connection_type is ConnectionType::ZDT, which is the
default. Ignored on a TCP session.
| Option | Default | Notes |
|---|---|---|
rto_min |
100 ms | Floor on the retransmit timeout, however low the measured RTT |
rto_max |
2000 ms | Ceiling, including backoff |
max_retries |
10 | Retransmits of one message before the connection is closed |
Lowering rto_min recovers from loss faster on a LAN. On the open internet it
causes retransmits of packets that were merely late, which costs bandwidth and
can push the congestion controller down.
| Option | Default | Notes |
|---|---|---|
max_datagrams_in_flight |
512 | Ceiling on the congestion window, in datagrams |
cwnd |
4096 | Reliable messages allowed in flight. A memory bound, not congestion control |
max_datagrams_in_flight is a bound, not the window. ZDT slow-starts from
10 datagrams and backs off on queueing delay rather than on loss; this caps how
far it may grow. It counts datagrams, not bytes, so a half-full datagram costs
as much as a full one. Bytes in flight are roughly this times the MTU per round
trip, which is what governs throughput on a long link.
The two are separate because coalescing puts many messages in one datagram.
Holding cwnd near max_datagrams_in_flight would throttle small messages far
below what the window actually allows.
| Option | Default | Notes |
|---|---|---|
mtu_ladder |
1492, 1200, 576 | Candidates, probed largest first. Set with .Set({...}), max 4 |
handshake_retransmit |
250 ms | Wait for a reply before resending |
handshake_retries_per_rung |
4 | Attempts at one MTU before stepping down |
The settled MTU determines the largest message the connection can carry. See Choosing a Transport.
| Option | Default | Notes |
|---|---|---|
cookie_secret_rotation |
120 s | How often the server rotates its return-routability secret |
max_half_open |
1024 | Concurrent half-open handshakes a server tracks |
max_connections |
4096 | Established connections a server holds |
per_source_handshake_rate |
20/s | Handshake messages accepted per source address |
reassembly_timeout |
5 s | Discard a partial message after this |
max_reassembly_bytes |
16 MiB | Ceiling on bytes held in partial reassemblies |
max_reassemblies |
256 | Concurrent partially reassembled messages |
max_inbox_datagrams |
4096 | Raw datagrams queued per connection before arrivals are dropped |
outbound_queue_capacity |
4096 | Encoded messages the transport holds before Send() fails |
Reaching max_reassembly_bytes refuses to start new messages rather than
discarding data already accepted, so a peer stalls instead of losing anything.
outbound_queue_capacity sits past the point where a caller can be told to try
again: the message is already encoded, so a refusal here drops it rather than
pushing back the way a full send_queue_capacity does. That is why its default
is generous rather than tuned. The transport also keeps a staging queue, where a
shut congestion window parks messages, and refills from the ring only while it
holds fewer than this many, so it can hold up to twice this figure in total.
| Option | Default | Notes |
|---|---|---|
socket_recv_buffer |
4 MiB |
SO_RCVBUF on the UDP socket. Zero leaves the OS default |
socket_send_buffer |
4 MiB |
SO_SNDBUF, on the same terms |
One socket serves every connection on an endpoint, so these are per endpoint,
not per session. They only have to absorb bursts that arrive between drains by
the receive thread; max_inbox_datagrams is the designed backpressure point,
not these.
Both are best-effort. The kernel clamps silently to its own ceiling
(net.core.rmem_max and net.core.wmem_max on Linux, often well below 4 MiB
out of the box), so asking for more than it allows is not an error and warns
about nothing. znet reads the granted size back and logs both at debug level:
ZDT socket buffers: asked 4194304/4194304, granted 425984/425984 (recv/send bytes, ...)
Raise the sysctls if the larger ask has to take effect. Going much beyond what covers a scheduler stall buys little: the receive thread's drain rate is the bottleneck, and a very deep kernel queue makes every session wait behind a flood rather than letting the per-session inbox caps drop it fairly.
Listener scope: things that exist before any session does.
| Option | Default | Notes |
|---|---|---|
backlog |
0 | Pending-connection backlog. Zero uses SOMAXCONN. TCP only |
max_connections |
0 | Cap on concurrent sessions, refused at accept. Zero means unlimited |
reuse_address |
true |
SO_REUSEADDR on the listening socket |
allowlist |
empty | Sources allowed to connect, as CIDRBlocks. Empty admits everyone the denylist does not refuse |
denylist |
empty | Sources always refused. Wins over the allowlist |
max_attempts_per_source |
0 | Connection attempts one source IP may make per attempt_window. Zero disables |
attempt_window |
10 s | The window the attempt count runs over |
Note that ZDTOptions::max_connections and ServerOptions::max_connections are
different settings: the ZDT one is enforced by the transport before a session
exists, the listener one by the server.
The lists and the throttle are checked when a connection arrives: at accept on
TCP, at first contact on ZDT, where refusal is a silent drop so an excluded
source learns nothing. Rules are CIDRBlocks:
config.options.denylist.push_back(znet::CIDRBlock::Parse("203.0.113.0/24"));
config.options.allowlist.push_back(znet::CIDRBlock::Parse("10.0.0.0/8"));
config.options.max_attempts_per_source = 10;A bare host parses as its /32 (or /128). IPv4 rules also match IPv4-mapped
IPv6 sources, which is what a v4 client looks like to a dual-stack listener.
Invalid blocks are dropped with an error at server construction, not enforced
half-parsed. Unix socket listeners have no source address and bypass all of
it; the socket file's permissions are their gate. On ZDT the attempt throttle
counts every handshake-opening datagram, including retransmits of a lost one,
so leave slack above the honest rate. Refusals show up as the
admission_rejected counter in Metrics.