Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Network

This page covers the layer under TLS: how a host name becomes an address, how Leyline picks between IPv4 and IPv6, which socket options it sets, and how the connection pool and the HSTS store work.

Map a host to an address

DnsConfig::resolve_host sends one host to a fixed list of addresses without a DNS lookup. Leyline replaces the port of each address with the port of the request URL, so the port you give is a placeholder. The host name still goes into SNI and the Host header, so the server sees a normal request.

#![allow(unused)]
fn main() {
fn run() -> leyline::Result<()> {
use leyline::DnsConfig;

let dns = DnsConfig::new().resolve_host(
    "api.example.com",
    [
        "203.0.113.10:0".parse().unwrap(),
        "203.0.113.11:0".parse().unwrap(),
    ],
);
let session = leyline::Session::builder()
    .browser(leyline::Browser::default())
    .dns(dns)
    .build()?;
let _ = session;
Ok(())
}
}

Hosts without an override go to the resolver. A later call for the same host replaces its list, and an empty list removes the override. Put every override in one DnsConfig: a second dns call replaces the first.

Replace the resolver

The default is tls::SystemResolver. To use your own, implement tls::Resolver and pass it to SessionBuilder::dns as an Arc<dyn Resolver>. DnsConfig holds the resolver and the overrides as one value, so several builders can share it.

Happy Eyeballs

Leyline connects to the addresses of a host IPv6 first, then alternating between the families. Inside one family, the order stays as the resolver or resolve_host gave it. Leyline starts one attempt, and starts the next when resolve_delay passes with no connection, or at once when every running attempt has failed. The first connection to complete wins.

This applies to every TCP connection, mapped hosts included. A direct HTTP/3 connection uses one address: the first IPv4 address, or the first address when the host has none.

tls::HappyEyeballsConfigDefaultMeaning
resolve_delay250 msWait before Leyline starts the next address
attempt_limit8Most addresses tried for one connect
#![allow(unused)]
fn main() {
fn run() -> leyline::Result<()> {
use std::time::Duration;
use leyline::SocketConfig;
use leyline::tls::HappyEyeballsConfig;

let he = HappyEyeballsConfig::new()
    .resolve_delay(Duration::from_millis(100))
    .attempt_limit(4);
let session = leyline::Session::builder()
    .browser(leyline::Browser::default())
    .socket(SocketConfig::new().happy_eyeballs(he))
    .build()?;
let _ = session;
Ok(())
}
}

Happy Eyeballs cannot be turned off; attempt_limit(1) tries one address only. happy_eyeballs(None) keeps the defaults.

Socket options

SocketConfig sets options on each TCP socket before it connects. SessionBuilder::socket replaces the whole value, so start from SocketConfig::new(), which carries the defaults.

MethodDefaultEffect
tcp_nodelayunsetTCP_NODELAY
tcp_keepalive60 sIdle time before the first keepalive probe
tcp_keepalive_interval30 sTime between probes
tcp_keepalive_retries3Failed probes before the socket closes
tcp_user_timeoutunsetTCP_USER_TIMEOUT: how long sent data may stay unacknowledged (Linux, Android)
send_buffer_size, recv_buffer_sizeunsetSO_SNDBUF, SO_RCVBUF
local_addressunsetSource IP to bind
local_ipv4, local_ipv6unsetSource IP to bind for that address family
strictfalseFail when the platform does not support an option
happy_eyeballssee aboveHappy Eyeballs settings

The timing and size setters take a value or None, which clears it. Leyline sets TCP keepalive when any of its three values is set; to turn it off, pass None to all three.

#![allow(unused)]
fn main() {
fn run() -> leyline::Result<()> {
use std::time::Duration;
use leyline::SocketConfig;

let session = leyline::Session::builder()
    .browser(leyline::Browser::default())
    .socket(
        SocketConfig::new()
            .tcp_nodelay(true)
            .tcp_user_timeout(Duration::from_secs(20))
            .tcp_keepalive(Duration::from_secs(30))
            .tcp_keepalive_interval(Duration::from_secs(10)),
    )
    .build()?;
let _ = session;
Ok(())
}
}

Set tcp_user_timeout when a pooled connection can die without a FIN or RST, for example when a proxy or NAT drops its state. Without it, the kernel retransmits for many minutes and the request waits for its total timeout. With it, the kernel closes the socket, the request fails, and a retry policy can use a fresh connection.

Where an option is unsupported, Leyline logs one warning for it on the leyline::socket target and continues. With strict(true), the connect fails with an io::ErrorKind::Unsupported error. tcp_keepalive_retries falls back to the operating system default where it cannot be set.

SocketConfig is separate from TcpProfile, which sets the values of the JA4T fingerprint and comes from the session platform. See Fingerprints.

Connection pool

Each session has a connection pool. PoolConfig sets its limits, and every limit applies to one pool.

SettingDefaultEffect
max_connections2048Pool entries. The least recently used entry leaves first
max_h1_conns_per_host256HTTP/1.1 connections per host, port, scheme, proxy, and transport. A request over the limit waits for a free slot
idle_timeout300 sAn idle connection closes after this time
keepalivetrueKeep connections open for reuse
h2_ping_after_idle10 sAn idle HTTP/2 connection gets a PING after this time. None turns it off
h2_ping_timeout2 sThe wait for the answer to that PING
#![allow(unused)]
fn main() {
use leyline::PoolConfig;

fn run() -> leyline::Result<()> {
let session = leyline::Session::builder()
    .browser(leyline::Browser::default())
    .pool(PoolConfig::new().max_connections(512).max_h1_conns_per_host(8))
    .build()?;
let _ = session;
Ok(())
}
}

Clones and every derived session share the pool, except fresh_pool, which starts a new, empty one. A with_identity session keeps its connections in a partition of its own, inside the shared pool and its limits. See Sessions.

When every stream slot of an HTTP/2 connection is in use, one request waits inside the connection and later requests wait in a queue of at most 1024. The request’s timeout covers the wait, and a request dropped while it waits is never sent.

One handshake per connection

An HTTPS connection always sends the profile’s ClientHello, with its ALPN list, for example h2, http/1.1, and its ALPS extension. The server’s ALPN choice decides HTTP/2 or HTTP/1.1 on that same connection. Leyline never dials again to change the protocol, so a streaming request body to an HTTP/1.1-only origin works on the first request.

When an origin negotiates HTTP/1.1, the pool remembers it for 10 minutes, for at most 1024 origins. The next connection still sends the full ClientHello, and if the origin then answers h2, Leyline uses HTTP/2 and forgets the entry.

ProtocolPolicy::Http2 sends every request over HTTP/2 with the same ClientHello; when the server picks another protocol, the request fails with Kind::Http2. ProtocolPolicy::Http1 and a WebSocket over HTTP/1.1 offer http/1.1 only.

Limit requests per host

HostLimits limits the requests to each origin, apart from the pool limits, which count connections. See Limit each host.

HSTS

Leyline keeps an HSTS store per session (RFC 6797). It records the first Strict-Transport-Security header of each https response. The header needs max-age; includeSubDomains also covers the subdomains, and max-age=0 removes the host. IP addresses are ignored. A session that turns off certificate verification records nothing, because it cannot trust the header.

A later http:// request or redirect to a recorded host, or a covered subdomain, goes to https instead, and an explicit port 80 becomes 443. WebSocket requests are not upgraded.

Derived sessions, with_identity included, share the store; only fresh_pool starts with an empty one. Session::state() saves the store, and SessionState::restore_into loads it. See Accounts.

Next

Read Logging and tracing. Timeouts are covered in Retries and timeouts.