v0.25.0 netacl and fixes
Test / test (push) Canceled after 0s
Test / e2e (push) Canceled after 0s

This commit is contained in:
lixen committed 2026-10-04 23:43:52 -04:00
1 parent 9e86d7999d
commit 4af072f4e5
30 files changed
+918 -228

No files matched your search

+11 -8
View File
@@ -162,11 +162,11 @@ Pipelines over stdin and stdout are under [Usage Patterns](#usage-patterns).
## Presets
`--preset NAME,key=value,...` takes the [SPEC](#pipelines) syntax. A list
key (`hosts`, `proxy`) takes several values, repeated (`hosts=a,hosts=b`) or
`,`-separated; any other key takes one. `lw preset NAME [--key value ...]`
prints the pipeline a preset expands to, ready for a configuration file; its
flags are the keys, spelled as in the SPEC (`--password_file`), and `-u` is
`--user`;
key (`hosts`, `proxy`, `allow`, `deny`) takes several values, repeated
(`hosts=a,hosts=b`) or `,`-separated; any other key takes one.
`lw preset NAME [--key value ...]` prints the pipeline a preset expands to,
ready for a configuration file; its flags are the keys, spelled as in the SPEC
(`--password_file`), and `-u` is `--user`;
`lw preset NAME -h` lists its keys and defaults, `lw preset` the presets. An
unknown key fails and lists the valid ones. A `path` is a file, a directory
(its files) or a glob; a preset that takes one reads stdin without it, and
@@ -183,6 +183,8 @@ unknown key fails and lists the valid ones. A `path` is a file, a directory
startup), `issuer` (one signed by `issuer_cert` and `issuer_key`), or
`files` (`cert` and `key`); `hosts` adds names to a made certificate
- without `users` the viewer needs no login, over plain http too
- `allow` and `deny`: addresses or CIDRs, the sink's
[acl](security.md#the-acl-block)
- `users`: a credentials file; readers then log in with SCRAM, and browsers
need `proxy` and `viewer=true`: the login page and viewer behind the
TLS-terminating proxies `proxy` names, since a browser cannot bind its
@@ -198,6 +200,7 @@ unknown key fails and lists the valid ones. A `path` is a file, a directory
- `listen` (`0.0.0.0:9000`), `transport` (`tcp`), `format` (`json`)
- `tls`: `self` (default), `issuer` or `files`, as for `serve`; never `off`
- authenticate by `users` (SCRAM) or `client_ca` (mTLS): one is required
- `allow` and `deny`, as for `serve`
- `out`: a directory for `aggregate*.log` files; default stdout
```bash
@@ -349,9 +352,9 @@ values from the file, the environment or flags win.
finishes when all its sources have ended, and once every pipeline has
finished lw shuts down gracefully and exits 0. Only a console source ends on
its own: file and network sources run until a signal, and so does a pipeline
holding one. Every sink writes what is queued before the exit; the `http`
and `tcp` sinks give each connected client `write_timeout_ms`, at most 2 s,
to take it.
holding one. Every sink writes what is queued before the exit; a network
sink gives its clients or its downstream its `write_timeout_ms`
(`request_timeout_ms` for `http_chain`), at most 2 s, to take it.
## Usage Patterns
+1 -1
View File
@@ -184,7 +184,7 @@ every key.
- filters, rate limits, heartbeat, formatter flags and timestamp layouts
- more than one pipeline; both chain transports, or a further hop, on one node
- proxy mode: browser logins through a TLS-terminating proxy
- network ACLs: not implemented in LogWisp yet
- `acl` address rules on the listeners
- mtls identities other than the certificate CN; `node_binding` other than
`force`
- creating certificates
+3
View File
@@ -289,6 +289,7 @@ or SKIP (exit 77, e.g. no IPv6 loopback); only a failure fails the target.
15831-15832 scram-proxy-test.sh test/run-proxy/
15851-15855 ipv6-test.sh test/run-ipv6/
15871-15879 deploy-test.sh test/run-deploy/
15881-15889 acl-test.sh test/run/acl/
```
- `chain-test.sh`: two edges into a relay, one pipeline per chain transport;
@@ -310,3 +311,5 @@ or SKIP (exit 77, e.g. no IPv6 loopback); only a failure fails the target.
loopback
- `deploy-test.sh`: the configurations `deploy/lw-deploy.sh` writes, deployed
and checked; ports 15871-15879
- `acl-test.sh`: each listener admitting or refusing peers by address before
TLS, with clients from 127.0.0.1 and 127.0.0.2; ports 15881-15889
+12 -3
View File
@@ -301,7 +301,7 @@ runs without a configuration.
.It Cm tail path Op Cm from , format
Follow files to standard output, like
.Ql tail \-F .
.It Cm serve Oo Cm path , listen , format , tls , users , proxy , viewer Oc
.It Cm serve Oo Cm path , listen , format , tls , users , proxy , viewer , allow , deny Oc
Serve a live HTTP stream on
.Cm listen
.Pq default Li 127.0.0.1:8080 .
@@ -324,6 +324,10 @@ only behind a TLS-terminating proxy named by
with
.Cm viewer=true ,
since a browser cannot bind its login to the TLS channel.
.Cm allow
and
.Cm deny
list the addresses or CIDRs the listener admits and refuses.
.It Cm edge to Oo Cm path , transport , ca | pin , user , password_file , cert , key , node Oc
Forward to an aggregator at
.Cm to
@@ -340,7 +344,7 @@ or a client
.Cm cert
and
.Cm key .
.It Cm aggregator Oo Cm listen , transport , tls , users , client_ca , out , format Oc
.It Cm aggregator Oo Cm listen , transport , tls , users , client_ca , out , format , allow , deny Oc
Receive from edges on
.Cm listen
.Pq default Li 0.0.0.0:9000
@@ -351,7 +355,12 @@ authenticated by the
credentials file or the client certificates
.Cm client_ca
vouches for, and write to standard output or to files in
.Cm out .
.Cm out ;
.Cm allow
and
.Cm deny
as for
.Cm serve .
.El
.Ss Certificate commands
.Nm
+13 -3
View File
@@ -117,9 +117,10 @@ 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](security.md#throttling)), 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.
[Security](security.md#throttling)), there is **no** per-IP limiting; `acl`
admits or refuses peers by address ([Security](security.md#the-acl-block)).
`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
@@ -206,6 +207,15 @@ headers, and entry encoding.
may resolve to either.
- Check the port is not already bound by another pipeline in the same process.
**Closed as soon as it connects, before TLS**
- The listener's `acl` refused the address: `acl_denied` rises, and its log has
`Connection refused by acl` at WARN, once a minute at most. The dialer sees
`EOF` or `connection reset by peer` (curl: exit 52 or 56).
- Behind a proxy (an L4 passthrough, or the `http` sink's
`auth.trusted_proxies`) every peer has the proxy's address: allow the proxy.
- `acl: allow entry "...": not of the listener's family` at startup — an IPv4
entry on an IPv6 `host`, or the reverse: each family's listener takes its own.
**TLS handshake failure**
- `client didn't provide a certificate` — the listener has `client_auth = true`
and the dialer has no `cert_file`/`key_file`.
+4 -4
View File
@@ -17,10 +17,10 @@ lw exits on its own only at the end of input: once every pipeline has
finished, a pipeline finishing when all its sources have ended. Only a console
source ends, when stdin does; file and network sources run until a signal, and
lw shuts down gracefully, exit 0, either way.
Every sink writes what is queued before the exit; the `http` and `tcp` sinks
give each client `write_timeout_ms`, at most 2 s, to take it. A reload stops
the old sinks the same way, so a client that stopped reading delays it by
that much. See
Every sink writes what is queued before the exit; a network sink gives its
clients or its downstream its `write_timeout_ms` (`request_timeout_ms` for
`http_chain`), at most 2 s, to take it. A reload stops the old sinks the same
way, so a stalled client or downstream delays it by that much. See
[CLI](cli.md#built-in-defaults).
There is no built-in daemon mode. Run LogWisp in the foreground under a
+48 -6
View File
@@ -26,10 +26,12 @@ configure it, and — equally important — what it does not yet do.
[Browsers behind a TLS-terminating proxy](#browsers-behind-a-tls-terminating-proxy).
- Server pinning by dialers: certificate identity (`mtls`), bound certificate
(`scram`).
- Address rules on every listener, applied before TLS: see
[The ACL Block](#the-acl-block).
- Startup warnings for expiring certificates and risky settings: see
[Startup Warnings](#startup-warnings).
- Unknown configuration keys rejected: a typo in `tls`, `auth` or a table path
fails startup.
- Unknown configuration keys rejected: a typo in `tls`, `auth`, `acl` or a
table path fails startup.
- Configuration input hardened (lixenwraith/config v0.2.2 and
lixenwraith/toml v0.1.3, reviewed with adversarial tests and fuzzing like
`auth`):
@@ -49,8 +51,10 @@ configure it, and — equally important — what it does not yet do.
- Certificate revocation lists (CRL) or OCSP: revoke by editing the allow-list
or credentials file.
- IP allow/deny lists, per-IP connection or request limits: only SCRAM logins
are throttled per address.
- Per-address connection or request limits: only SCRAM logins are throttled
per address.
- Address rules for clients behind a proxy: `acl` sees the proxy, never the
client a PROXY header or `X-Forwarded-For` names.
- PROXY protocol: behind a proxy that passes TLS through, every peer shares the
proxy's address; see
[Behind nginx or another proxy](#behind-nginx-or-another-proxy).
@@ -319,6 +323,36 @@ Misconfiguration fails at plugin construction, before the pipeline starts:
Errors read like `auth: type "mtls" requires tls.client_auth`.
## The ACL Block
`acl`, beside `tls` and `auth` on the four listeners (`tcp` and `http` sinks,
`tcp_chain` and `http_chain` sources), refuses peers by address as the socket
is accepted: before the TLS handshake or a byte of the protocol, so a refused
peer costs no handshake and never reaches `auth`.
```toml
[pipelines.plugin_sources.config.acl]
allow = ["192.0.2.0/24", "198.51.100.7"] # addresses or CIDRs; empty: all
deny = ["192.0.2.66"] # refused, listed in allow or not
```
- `deny` wins; then a non-empty `allow` admits only its entries, an empty one
every address `deny` does not list.
- Entries are of the listener's [family](networking.md#address-family): IPv4
on an IPv4 listener, IPv6 on an IPv6 one, either behind a hostname (only the
family it binds ever matches). An entry of the other family, an IPv4-mapped
address, one with a zone, or anything but an address or CIDR fails
construction. A link-local peer matches without its zone.
- A refused connection is closed at once and counted in `acl_denied` (plugin
stats, the `http` sink's status). A WARN names the peer and the count, at
most once a minute per listener; the refusals in between are only counted.
- The rules see the socket peer: behind a proxy (an L4 passthrough, or the
`http` sink's `trusted_proxies`), the proxy (see
[Behind nginx or another proxy](#behind-nginx-or-another-proxy)).
- The `serve` and `aggregator` presets take `allow` and `deny` list keys.
- Dialers have no `acl`: on a `tcp_chain` or `http_chain` sink it is an
unknown key.
## Startup Warnings
Some settings work but are usually mistakes. Each plugin reports them at WARN
@@ -334,6 +368,9 @@ when it is constructed, so startup and every reload repeat them:
- a `key_file`, `issuer_key_file`, `credentials_file` or `password_file` every
local user can read
(once per path per process)
- an `acl` entry with host bits set (`10.0.0.1/8`), naming the network it
matches; an `acl.allow` entry of a whole family (`0.0.0.0/0`, `::/0`); a
wildcard listener whose `acl` has `deny` but no `allow`
## Certificates made at startup
@@ -816,7 +853,8 @@ stream {
(`too many attempts` or `busy`) keeps sending on its token to the server it
pinned, retrying every few seconds, and holds its batches only once that
token has expired; any other refusal ends the token at once.
- Logs and sessions name the proxy, not the peer.
- Logs and sessions name the proxy, not the peer, and `acl` rules see only the
proxy: allow it, not the clients.
- Proxy mode is unaffected: it throttles on the forwarded client.
- Until LogWisp reads PROXY, mitigate:
- expose the chain ports directly, without the proxy, where you can;
@@ -845,6 +883,9 @@ compromised edge cannot attribute its entries to another host, and the `http`
sink's stream and status endpoints stop being open to anyone who can reach the
port.
**`acl`** — an address check, per listener, before TLS: it narrows who can
reach the other layers, never proves who a peer is.
**Revocation** is the allow-list or the credentials file, not a CRL. Remove the
identity or user and send `SIGHUP`: the reload rebuilds every pipeline, so the
change takes effect on the next connection and existing ones are dropped by the
@@ -856,7 +897,8 @@ would add.
## Surfaces Without Access Control
An `auth` block (`mtls` or `scram`) closes each of these. Without one, bind them
to a trusted interface or front them with an authenticating proxy.
to a trusted interface, front them with an authenticating proxy, or narrow them
to known networks with `acl.allow`.
What each exposes when `auth.type = "none"`:
+18 -7
View File
@@ -178,6 +178,8 @@ Options, each as type and default:
- `tls` (table): listener TLS; see [Security](security.md).
- `auth` (table): client authentication (`mtls` or `scram`); see
[Security](security.md#the-auth-block).
- `acl` (table): `allow` and `deny`, addresses or CIDRs admitted and refused
before TLS; see [Security](security.md#the-acl-block).
**Behaviour**
@@ -241,15 +243,16 @@ Options, each as type and default:
- HTTP/2 is negotiated via ALPN when TLS is enabled; plaintext is HTTP/1.1.
**Status endpoint** returns service and version identity, host, port, TLS flag,
the compiled auth policy, active client count, sink and per-client buffer sizes,
connection limit, write timeout, uptime, endpoint paths, and the
`total_processed` / `dropped_writes` / `rejected_clients` / `auth_rejected`
counters.
the compiled auth policy and acl, active client count, sink and per-client
buffer sizes, connection limit, write timeout, uptime, endpoint paths, and the
`total_processed` / `dropped_writes` / `rejected_clients` / `auth_rejected` /
`acl_denied` counters.
**Statistics**: `dropped_writes`, `rejected_clients`, `auth`, `auth_allowed`,
`auth_rejected`; under `scram` also `auth_users`, `auth_throttled`,
`auth_busy`, `auth_binding_mismatch` and `auth_token_lifetime_ms`, and in
proxy mode `auth_trusted_proxies`.
proxy mode `auth_trusted_proxies`; with `acl` rules also `acl` and
`acl_denied`.
> Without an `auth` block both endpoints are unauthenticated, and the stream
> response carries `Access-Control-Allow-Origin: *`, so any web origin can read
@@ -302,6 +305,8 @@ Options, each as type and default:
- `tls` (table): listener TLS.
- `auth` (table): client authentication (`mtls` or `scram`); see
[Security](security.md#the-auth-block).
- `acl` (table): `allow` and `deny`, addresses or CIDRs admitted and refused
before TLS; see [Security](security.md#the-acl-block).
**Behaviour**
@@ -327,7 +332,8 @@ Options, each as type and default:
**Statistics**: `write_errors`, `dropped_writes`, `rejected_conns`,
`tls_handshake_errors`, `auth`, `auth_allowed`, `auth_rejected`; under `scram`
also `auth_users`, `auth_throttled`, `auth_busy` and `auth_binding_mismatch`.
also `auth_users`, `auth_throttled`, `auth_busy` and `auth_binding_mismatch`;
with `acl` rules also `acl` and `acl_denied`.
---
@@ -391,6 +397,9 @@ Options, each as type and default:
exponential backoff plus ±20 % jitter between attempts. The delay keeps
growing until a link outlives `backoff_min_ms`, so a source that drops every
link right after accepting it is not retried in a tight loop.
- On shutdown (a reload too) the sink delivers what is queued, connecting
first if it must, within `write_timeout_ms`, at most 2 s; then the link
closes and what is left is lost.
- A source never writes once a link is up, so the sink watches each link: a
line from the source is a refusal (a sink without `scram` facing a `scram`
source), logged at WARN as `Chain link refused`; EOF ends the link at once
@@ -477,7 +486,9 @@ Options, each as type and default:
- Redirects are never followed; a `3xx` is permanent too. Following one would
resend the batch wherever the response points, plaintext `http` included.
- HTTP/2 is off by design; batched NDJSON POSTs gain nothing from it.
- On shutdown a single best-effort flush of the pending batch is attempted.
- On shutdown (a reload too) the sink delivers what is queued and batched
within `request_timeout_ms`, at most 2 s; each batch still undelivered then
is dropped and logged at WARN.
**Statistics**: `target`, `node`, `tls`, `auth`, `batches_sent`,
`request_errors`, `dropped_batches`, `synthesized`; under `scram` also
+7 -2
View File
@@ -214,6 +214,8 @@ Options, each as type and default:
- `tls` (table): listener TLS; see [Security](security.md).
- `auth` (table): peer authentication (`mtls` or `scram`) and node binding;
see [Security](security.md#the-auth-block).
- `acl` (table): `allow` and `deny`, addresses or CIDRs admitted and refused
before TLS; see [Security](security.md#the-acl-block).
For passwords instead of certificates, the `auth` block takes `type = "scram"`
and a `credentials_file`, and `client_auth` becomes optional; see
@@ -247,7 +249,7 @@ and a `credentials_file`, and `client_auth` becomes optional; see
**Statistics**: `active_connections`, `rejected_conns`, `parse_errors`,
`tls_handshake_errors`, `trust_node`, `auth`, `auth_allowed`, `auth_rejected`,
`node_binding`; under `scram` also `auth_users`, `auth_throttled`, `auth_busy`
and `auth_binding_mismatch`.
and `auth_binding_mismatch`; with `acl` rules also `acl` and `acl_denied`.
---
@@ -298,6 +300,8 @@ Options, each as type and default:
- `tls` (table): listener TLS; see [Security](security.md).
- `auth` (table): peer authentication (`mtls` or `scram`) and node binding;
see [Security](security.md#the-auth-block).
- `acl` (table): `allow` and `deny`, addresses or CIDRs admitted and refused
before TLS; see [Security](security.md#the-acl-block).
**Behaviour**
@@ -324,7 +328,8 @@ Options, each as type and default:
**Statistics**: `total_requests`, `rejected_requests`, `parse_errors`,
`cached_sessions`, `trust_node`, `auth`, `auth_allowed`, `auth_rejected`,
`node_binding`; under `scram` also `auth_users`, `auth_throttled`, `auth_busy`,
`auth_binding_mismatch` and `auth_token_lifetime_ms`.
`auth_binding_mismatch` and `auth_token_lifetime_ms`; with `acl` rules also
`acl` and `acl_denied`.
---
+27 -54
View File
@@ -6,29 +6,19 @@ they belong to.
## 1. Network access control (ACL)
The next feature push. Today a listener decides *who* a peer is (`tls`, `auth`);
nothing decides *where* a peer may connect from, and every limit except SCRAM
throttling is global. The ACL adds address rules and per-client limits at the
connection level, below TLS, so a refused peer costs no handshake.
The seam exists: `internal/netacl` wraps each listener's socket before TLS,
and the `acl` block's `allow` and `deny` refuse peers by address
([Security](security.md#the-acl-block)). Every limit except SCRAM throttling
is still global, and behind an L4 proxy every peer is the proxy.
### 1.1 One seam: a filtering listener
### 1.1 The filtering listener
- New package `internal/netacl`, the third network seam beside `tlsx` and
`authz` (AGENTS.md lists the seams; add it there).
- `netacl.New(opts *config.ACLOptions, role) (*Policy, error)` compiles the
block; a nil policy is valid and transparent, like `authz`.
- `(*Policy).Listener(ln net.Listener) net.Listener` wraps the raw listener
before TLS:
- TCP plugins wrap it before `tls.NewListener`.
- HTTP plugins wrap it before `Serve`/`ServeTLS`.
- `Accept` reads an optional PROXY header (1.2), decides allow or deny (1.3),
applies per-address connection caps (1.4), and returns a `net.Conn` whose
`RemoteAddr` is the real client.
- `Accept` is to read an optional PROXY header (1.2) before the address rules
and apply per-address connection caps (1.4) after them, returning a
`net.Conn` whose `RemoteAddr` is the real client.
- Everything downstream then sees the real client with no further change:
`authz.remoteIP`, SCRAM throttling, sessions, logs and `http.Request.RemoteAddr`.
- Keep the proxy's own address as `peer_addr` in session metadata for audit.
- A denied connection is closed before any byte is read or written, with a
counter (`acl_denied`) and a rate-limited WARN.
### 1.2 PROXY protocol (deferred gap of SCRAM, see scram-auth-plan.md)
@@ -64,20 +54,14 @@ connection level, below TLS, so a refused peer costs no handshake.
one deployment can use both: nginx `stream` with `proxy_protocol` into an
nginx `http` block that sets `X-Forwarded-For $proxy_protocol_addr`.
### 1.3 Address rules
### 1.3 Address rules for forwarded clients
- `allow = [CIDR...]`, `deny = [CIDR...]`, IPv4 and IPv6 (strict per family,
as the listeners are).
- Order: deny wins, then allow; an empty allow list admits everyone not denied.
- Rules match the real client (after PROXY), never the proxy.
- In HTTP proxy mode, `X-Forwarded-For` is applied by `authz.ClientAddr` after
the connection-level decision.
- Address rules for forwarded HTTP clients therefore belong in a second,
request-level check inside `authz.ClientAddr`: reuse the compiled `netacl`
rules there rather than duplicating them.
- Startup warnings (in `LogStartup` style): an allow list containing
`0.0.0.0/0` or `::/0`, `proxy_from` covering public ranges, a listener on a
wildcard host with no `allow`.
- In HTTP proxy mode, `X-Forwarded-For` is applied by `authz.ClientAddr` after
the connection-level decision, so rules for forwarded HTTP clients belong in
a second, request-level check there, reusing the compiled `netacl` rules.
`authz.parseProxies` then takes netacl's address-or-CIDR parsing too.
- Startup warning: `proxy_from` covering public ranges.
### 1.4 Per-client limits
@@ -95,15 +79,11 @@ connection level, below TLS, so a refused peer costs no handshake.
### 1.5 Config, validation, docs
- Config shape: `[...config.acl]` beside `tls` and `auth` on the four listener
plugins.
- Decode through `config.Scan`, so unknown keys fail.
- Dialers have no ACL block.
- Intent rule as in `authz`: an ACL key on a dialer, or `proxy_from` without
`proxy_protocol`, is an error.
- Stats: `acl_denied`, `acl_proxy_headers`, `acl_limited`, merged into the
plugin stats like `authz.Stats`.
- Docs: a security.md section, plus networking.md troubleshooting lines for
- `proxy_protocol`, `proxy_from` and the 1.4 keys join `allow` and `deny` in
the `acl` block.
- Intent rule as in `authz`: `proxy_from` without `proxy_protocol` is an error.
- Stats: `acl_proxy_headers` and `acl_limited` beside `acl_denied`.
- Docs: the security.md section, plus networking.md troubleshooting lines for
each refusal.
### 1.6 Verification
@@ -112,15 +92,13 @@ connection level, below TLS, so a refused peer costs no handshake.
- PROXY v1 and v2 parse, including malformed, oversized and slow headers.
- A header from an unlisted peer refused; `required` without a header
refused; `LOCAL` kept.
- Deny over allow; IPv6 rules.
- Per-client connection cap; the limiter table failing closed.
- The real client reaching SCRAM throttling and session metadata.
- `test/acl-test.sh`:
- `test/acl-test.sh`, which covers the address rules, adds:
- A Go stub proxy that sends v1 and v2 headers in front of each listener
type.
- nginx `stream` with `proxy_protocol on` when nginx is installed.
- Denied and allowed clients; per-client SCRAM budgets behind one proxy
address.
- Per-client SCRAM budgets behind one proxy address.
## 2. Packaging: AUR, FreeBSD ports, Debian
@@ -183,21 +161,16 @@ identity, and both skeletons download the tagged source.
Smaller items found along the way; each is independent of the ACL.
1. Chain sinks at the end of a finite input. The `tcp_chain` sink's `Stop`
closes its loop without draining its queue, and the `http_chain` sink makes
one best-effort flush, so `seq 1 N | lw --preset edge,...` may lose the
tail. Verify with a finite stdin into each and an aggregator counting lines;
give both a bounded drain like `sink.FlushBound`.
2. A late browser sees no backlog: the `http` sink keeps none, so the viewer
1. A late browser sees no backlog: the `http` sink keeps none, so the viewer
shows entries from when it connects. A bounded replay (a `replay_lines`
option, sent after `event: connected`) would fill it. The viewer also hides
a `503` from `max_connections` behind "retrying"; the status JSON it already
fetches could name it.
3. Level detection. `source.ExtractLogLevel` needs a delimiter after the name
2. Level detection. `source.ExtractLogLevel` needs a delimiter after the name
(`ERROR:`, `[WARN]`, ` INFO `), so `warning disk full` or `DBG x` get no
level, hence no color. Match the names as words, as the console sink's
painter does, with one table for both.
4. Command line:
3. Command line:
- a positional word at the top level (`lw tail`, `lw -t FILE`) still reaches
config with an unclear message; name it ("unexpected argument X; the file
is -c FILE");
@@ -207,11 +180,11 @@ Smaller items found along the way; each is independent of the ACL.
- `lw tls cert --host` could become `--hosts` (keeping `--host`), to match
`tls.hosts` and the preset key;
- top-level usage errors exit 1, subcommand ones 2.
5. `GET /favicon.ico` answers 404; the logo could serve as the icon once the
4. `GET /favicon.ico` answers 404; the logo could serve as the icon once the
pages' CSP allows `img-src 'self'`.
6. TLS: the generated-certificate key is one per process, so a pin taken from a
5. TLS: the generated-certificate key is one per process, so a pin taken from a
`self_signed` listener also matches an `issuer` listener of the same
process; document it or key per listener. Pins are checked in
`VerifyPeerCertificate`, which Go skips on resumption: guard it if a dialer
ever keeps a `ClientSessionCache`.
7. `core.ShutdownTimeout` is unused.
6. `core.ShutdownTimeout` is unused.