13 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
All listeners bind tcp4 and all dialers dial tcp4. IPv6 is not
supported, deliberately. An IPv6 client cannot connect and will simply see a
connection failure.
When testing locally use 127.0.0.1, not localhost — the latter may resolve
to ::1 and appear as an unexplained connection refusal.
Network Plugins
| Plugin | Role | Protocol | Purpose |
|---|---|---|---|
tcp sink |
Listener | Raw stream | Broadcast formatted payloads to clients |
http sink |
Listener | HTTP SSE | Browser-friendly live stream plus status JSON |
tcp_chain source |
Listener | Chain v1 | Ingest a persistent NDJSON stream |
http_chain source |
Listener | Chain v1 | Ingest NDJSON batches over POST |
tcp_chain sink |
Dialer | Chain v1 | Forward entries over a persistent connection |
http_chain sink |
Dialer | Chain v1 | Forward 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. Zero means "no deadline" wherever the table says so.
| Plugin | Option | Default | Bounds |
|---|---|---|---|
tcp sink |
write_timeout_ms |
5000 |
One write to one client; a miss disconnects that client |
http sink |
write_timeout_ms |
0 (none) |
One SSE event write |
tcp_chain source |
hello_timeout_ms |
10000 |
Reading the protocol preamble, and under scram the whole login |
tcp_chain source |
read_timeout_ms |
0 (none) |
Idle time between entries |
http_chain source |
read_timeout_ms |
30000 |
Reading a whole request body |
tcp_chain sink |
dial_timeout_ms |
5000 |
TCP connect |
tcp_chain sink |
write_timeout_ms |
5000 |
One line write |
http_chain sink |
request_timeout_ms |
10000 |
Dial plus write plus response, and a SCRAM login when one is due |
Fixed, non-configurable bounds:
| Bound | Value | Applies to |
|---|---|---|
| TLS handshake | 10 s | All TLS listeners and dialers |
| HTTP read-header timeout | 10 s | http sink, http_chain source |
| HTTP server shutdown grace | 2 s | http sink, http_chain source |
| Max single entry line | 1 MiB | Chain listeners |
| SCRAM login | 10 s | tcp sink, chain sinks, logwisp auth, each /auth request |
SCRAM line or /auth body |
4 KiB | scram listeners 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 you are dialing IPv4.
localhostmay resolve to::1. - 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.- Handshake failures appear as WARN with the remote address, and increment
tls_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 needslogwisp 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 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 withlogwisp 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.
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).