16 KiB
Networking
Everything LogWisp does over a socket, and the knobs that shape it. For certificates and trust see Security; for multi-node topologies see Chaining.
Address Family
Every listener and dialer keeps strictly to the family of its host:
- IPv4 literal (
127.0.0.1, the default0.0.0.0): IPv4 only (tcp4). An emptyhostis the IPv4 wildcard, as0.0.0.0. - IPv6 literal (
::1,::,fe80::1%eth0): IPv6 only (tcp6).::takes no IPv4 connections, on Linux and FreeBSD alike, whatever the system'sbindv6onlydefault.- To serve both families, run two listeners on the same port, one on
0.0.0.0and one on::: neither holds the other's port.
- Hostname: resolved. A listener binds one address (IPv4 when there is one)
in that address's family alone, a name for
0.0.0.0or::too, so name the address to be sure; a dialer tries each address in turn.
IPv6
- Notation
hosttakes the bare address:host = "::1". A bracketed host, one with a port, or an IPv4-mapped one (::ffff:10.0.0.1) fails at load.- Addresses and URLs bracket it: logs print
[::1]:8443, andlw authtakes-addr [::1]:8443and-url https://[::1]:8443. Oldercurlreleases read the brackets as a glob;-gstops that and is harmless. - A link-local address carries its zone,
fe80::1%eth0, escaped in URLs ashttps://[fe80::1%25eth0]:8443.
- TLS: a dialer verifies an IPv6 target against the certificate's IP SANs
(
subjectAltName = IP:::1), as it does an IPv4 one. The zone is not part of the name;server_nameoverrides as usual. - Peers
- Logs and sessions name an IPv6 peer by its address, a link-local one with its zone.
- SCRAM throttling counts an IPv6 client by its /64, so the hosts on a link
share one link-local budget per interface (zone): each can pick any
fe80::/64address. - A peer with a zone never matches
auth.trusted_proxies: put the proxy on loopback or a routed address.
Network Plugins
Listeners:
tcpsink: raw stream; broadcasts formatted payloads to clients.httpsink: HTTP SSE; a browser-friendly live stream plus status JSON.tcp_chainsource: chain v1; ingests a persistent NDJSON stream.http_chainsource: chain v1; ingests NDJSON batches over POST.
Dialers:
tcp_chainsink: chain v1; forwards entries over a persistent connection.http_chainsink: chain v1; forwards entries as batched POSTs.
There is no port registry and no default port: port is required on every
network plugin. There is also no cross-pipeline conflict detection — two sinks
on the same port fail at bind time when the pipeline starts:
ERROR msg="Failed to start sink" error="tcp sink bind 0.0.0.0:9090: listen tcp4 0.0.0.0:9090: bind: address already in use"
Timeouts
Every network plugin exposes the deadlines relevant to its role. 0 means no
deadline where the default is 0 (none); elsewhere it selects the default.
tcpsinkwrite_timeout_ms, default5000: one write to one client; a miss disconnects that client.
httpsinkwrite_timeout_ms, default0(none): one SSE event write.
tcp_chainsourcehello_timeout_ms, default10000: reading the protocol preamble, and underscramthe whole login.read_timeout_ms, default0(none): idle time between entries.
http_chainsourceread_timeout_ms, default30000: reading a whole request body.
tcp_chainsinkdial_timeout_ms, default5000: TCP connect.write_timeout_ms, default5000: one line write.
http_chainsinkrequest_timeout_ms, default10000: dial plus write plus response, and a SCRAM login when one is due.
Fixed, non-configurable bounds:
- TLS handshake: 10 s, on all TLS listeners and dialers.
- HTTP read-header timeout: 10 s, on the
httpsink andhttp_chainsource. - HTTP server shutdown grace: 2 s, on the
httpsink andhttp_chainsource. - Single entry line: at most 1 MiB, on chain listeners.
- SCRAM login: 10 s, on the
tcpsink, chain sinks,lw auth, and each/authrequest. - SCRAM line or
/authbody: at most 4 KiB, onscramlisteners and dialers.
The http sink deliberately leaves the server's WriteTimeout unset, since it
would terminate long-lived SSE streams; per-event deadlines come from
write_timeout_ms instead.
Connection Limits
max_connections caps concurrent connections on the tcp sink, the http
sink, and the tcp_chain source. 0 means unlimited.
- Admission is a load-then-check, so a burst can over-admit by roughly one connection. This is accepted, not a bug to work around.
- On the
tcpsink andtcp_chainsource the count is taken at accept, so it bounds concurrent TLS handshakes as well as established sessions. - Over-limit connections are closed immediately and counted in
rejected_conns(TCP) orrejected_clients(HTTP, which first answers503).
The http_chain source has no connection cap; it bounds work with
max_body_bytes and read_timeout_ms instead.
Apart from SCRAM logins, which are throttled per address (see
Security), there is no per-IP limiting and no IP
allow/deny list. flow.rate_limit is a pipeline-wide entry rate limit, not a
network-level one — it cannot distinguish or throttle an individual peer.
Keep-Alive
TCP keep-alive is available on the tcp sink (for accepted connections) and the
tcp_chain sink (for its outbound connection):
keep_alive = true
keep_alive_period_ms = 30000
This is kernel-level keep-alive; it detects a dead peer but does not keep an application-level stream flowing. For that, use a heartbeat.
Heartbeats
Heartbeats are a flow-level feature, not a per-sink one. Enabling one injects a synthetic entry into the pipeline at a fixed interval; it reaches every sink and traverses chain links as an ordinary structured entry.
[pipelines.flow.heartbeat]
enabled = true
interval_ms = 30000
include_timestamp = true
include_stats = false
format = "txt" # txt | json | raw
Use it to keep idle SSE clients, TCP clients, and chain links from being reaped by intermediate NAT or proxy timeouts, and to make an idle pipeline visibly alive.
format = "comment"(SSE:comment framing) is rejected by validation despite appearing in older documentation and in a still-present code branch. A pipeline configured with it fails to start.
Reconnection
Chain sinks reconnect on their own. Both use exponential backoff between
backoff_min_ms and backoff_max_ms with ±20 % jitter, and both are
interruptible by shutdown.
backoff_min_ms = 500
backoff_max_ms = 30000
The connection is established lazily, so an edge node starts cleanly even when
its relay is down and connects as soon as the relay appears. Reconnect counts
are reported in the sink's reconnects statistic.
Server-side sinks (tcp, http) do not reconnect; clients are expected to
retry. Browsers reconnect SSE streams automatically.
Protocol Details
HTTP sink (SSE) — HTTP/1.1 in plaintext; HTTP/2 is negotiated via ALPN when
TLS is enabled. Only GET is routed to the stream and status paths. Each event
is framed as one data: line per newline in the payload, so multi-line entries
stream intact. Response headers set Cache-Control: no-cache,
X-Accel-Buffering: no, and, without an auth policy,
Access-Control-Allow-Origin: *. Under scram, POST /auth serves the login
and both paths take Authorization: Bearer <token>.
TCP sink — raw payload bytes, no framing added by the sink. Whether entries
are newline-delimited depends on the formatter. Under scram the client's hello
and login lines come first, as on a chain link.
Chain transports — see Chaining for the hello preamble, headers, and entry encoding.
Troubleshooting
Plugin fails to start with unknown key "..."
- The key is misspelled or belongs to another plugin type. Every plugin rejects
keys it does not declare, nested tables included, so a typo in
tlsorauthcannot leave a protection silently off.
Connection refused
- Confirm the pipeline started; a bind failure is logged at ERROR.
- Confirm the dialer uses the listener's family:
[::1]does not reach a127.0.0.1or0.0.0.0listener, nor127.0.0.1a::one.localhostmay resolve to either. - Check the port is not already bound by another pipeline in the same process.
TLS handshake failure
client didn't provide a certificate— the listener hasclient_auth = trueand the dialer has nocert_file/key_file.certificate signed by unknown authority— the dialer'sca_filedoes not contain the issuer of the server certificate, or the listener'sclient_ca_filedoes not contain the issuer of the client certificate.certificate is not valid for any names/ SAN mismatch — the dialedhostis not covered by the server certificate's SANs; setserver_name.protocol version not supported— one side is pinned tomin_version = "1.3"and the other cannot negotiate it.- Listeners log handshake failures at WARN with the remote address (the
tcpsink at DEBUG); thetcpsink andtcp_chainsource count them intls_handshake_errors.
Rejected after a successful handshake
auth: identity "..." is not allowed— the certificate is valid and chains to the CA, but the identity is not inauth.allow/auth.allow_patterns. On TCP the connection is closed; on HTTP the answer is403.auth: peer certificate carries no <mode> identity—auth.identitynames a field the certificate does not populate, e.g.san_dnson a CN-only leaf.auth: node_binding "assert": declared node "..." does not match identity— the sender'snodeoption and its certificate disagree. Fix one, or usenode_binding = "force"to let the certificate win silently.- On a
tcp_chainsink the same message insideChain connect refusedmeans the server was refused: its certificate identity is not in the sink'sauth.allow. - Rejections appear as WARN and increment
auth_rejected.
SCRAM login fails
authentication failed— the dialer is not told why, by design. The listener's WARN line (Connection rejected by auth policyon TCP,Login rejectedon HTTP) is:invalid credentialsfor a wrong password, an unknown user, or one removed or rotated on the listener (compare thepassword_filewith the lastadd-user, and check the listener gotSIGHUP);certificate cn "…" does not match user "…"for theidentitybinding.the client saw another server certificate: TLS interception or a terminating proxyin the listener's log, withauth_binding_mismatchrising — something between the peers terminates TLS. Pass TLS through to LogWisp; the login cannot work otherwise, except on anhttpsink in proxy mode (auth.trusted_proxies), wherethe client bound its proof to the proxy's certificatemeans a client that needslw auth token -unbound, andthe client sent an unbound proofthe reverse. On anhttp_chainsink,server certificate differs from the one the SCRAM login was bound tomeans the certificate changed after the login: a rotation (the retry binds anew), several backends, or interception.403from anhttpsink in proxy mode, withnot a trusted proxyorX-Forwarded-Protoin its WARN line — the request did not come from a listed proxy, or the proxy did not forwardX-Forwarded-Proto: httpsandX-Forwarded-For.too many attempts— the address failed or abandoned logins faster than one per second beyond a burst of 10, or has 4 unfinished; it clears within seconds once the failing peer stops. Peers behind one NAT or one passthrough proxy share the budget.busy— 4,096 logins in flight, or the listener is stopping.authentication not enabled— the dialer hasscram; the listener'sauth.typeisnoneormtls.no challenge within 10s (older logwisp, or not a scram listener)— the server read the hello and said nothing. Over HTTP the same case readsno auth endpoint at …/auth.Argon2 parameters below client minimum— the credentials file holds a cheaper Argon2 profile than the defaults; recreate it withlw auth.peer offered no credentials(authentication requiredon the wire) in the listener's log — a peer withoutscramreached ascramlistener; a client that sends nothing logsread hello: … i/o timeoutafter 10 s instead. Atcp_chainsink withoutscramlogs WARNChain link refusedwith that reason and retries under growing backoff; what it wrote before reading the refusal is lost. Anhttp_chainsink withoutscramgets401and drops every batch.
401 on HTTP under scram
- An
http_chainsink renews its token before it expires, so a401means the listener reloaded (every reload revokes all tokens); the retry logs in again. - Continuous
401means the token never sticks: several LogWisp processes behind one address, a proxy that stripsAuthorization, or atoken_lifetime_msshorter than a request takes. A token used withcurldies with every listener reload. 403is not the token: it is theidentitybinding, node binding or, in proxy mode, the proxy gate.
Behind an L4 proxy
- Every connection through the proxy fails: it sends a PROXY header (nginx
streamwithproxy_protocol on, HAProxysend-proxy) to a LogWisp TLS listener, which cannot read one yet. The header lands in front of the TLS handshake, which fails; the listener logs the proxy's address andfirst record does not look like a TLS handshake(thetcpsink at DEBUG). Route LogWisp without it: see Behind nginx or another proxy. - All peers share one throttling budget behind a passthrough proxy: without
PROXY every
scrampeer arrives from the proxy's address. One client failing logins exhausts everyone's, and a fifth login while 4 are unfinished (edges reconnecting together after a restart) is refused; both answertoo many attempts(Throttling). Expose the LogWisp ports directly, or keep the proxy hop on a trusted network so only trusted clients share the budget.
Entries not arriving over a chain link
- Check the sink's
connectedstatistic and itsreconnectscount. - Check the source's
auth_rejected— an allow-list miss or a failed login looks exactly like a network fault from the sender's side — and the sink'slast_auth_error. - Check the source's
parse_errors— a version skew shows up here. - On
http_chain, remember entries wait up toflush_interval_msbefore a batch is sent.
Entries arriving under an unexpected node label
auth.node_bindingdefaults toforcewith anyauthblock, which relabels every entry with the sender's certificate identity or username. If a dashboard suddenly shows a different label, that is why. Usenode_binding = "assert"to keep upstream origin labels on relay-to-relay hops, or"none"to leavetrust_nodein charge.
Clients connect but see nothing
- The pipeline may be filtering everything out; check
flow.filtersstats. - The rate limiter may be dropping everything; check
rate_limiterstats. - Nothing may be arriving from the sources; check source
total_entries.
Entries missing under load
- Compare
dropped_writes(per-client queue full — raiseclient_buffer_size) againsttotal_dropped_by_sink(sink input queue full — raisebuffer_sizeor reduce sink latency).