Files
logwisp/doc/todo.md
T
lixen bf410e1225
Test / test (push) Canceled after 0s
Test / e2e (push) Canceled after 0s
v0.23.0 color, presets, certificates made at startup, cli.go, sink flush
2026-10-04 03:44:12 -04:00

181 lines
8.5 KiB
Markdown

# To Do
Planned work, in order of priority. Each item names what to build, how it fits
the existing seams, and how to verify it. Finished items move to the design doc
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.
### 1.1 One seam: a 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.
- 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)
- Why: behind an L4 proxy that passes TLS through (nginx `stream` with
`ssl_preread`, HAProxy `mode tcp`), every peer arrives from the proxy's
address.
- All peers share one SCRAM throttling budget, so one client can lock out
every login.
- Logs, sessions and the ACL itself see only the proxy.
- What: accept PROXY v1 (text) and v2 (binary) headers, but only from
`proxy_from` addresses or CIDRs. Two config keys in the ACL block:
- `proxy_protocol = "off" | "optional" | "required"`, default off.
- `proxy_from = [...]`.
- Rules:
- A listed peer must send the header when `required`.
- An unlisted peer that sends a header is refused, since the header is
spoofable.
- The header must arrive within a short deadline (reuse
`tlsx.HandshakeTimeout`), with a size cap: v1 at most 107 bytes, v2 with a
bounded TLV length.
- `LOCAL`/`UNKNOWN` commands (proxy health checks) keep the socket address.
- TLVs are ignored.
- The header precedes the TLS ClientHello, so SCRAM channel binding is
unaffected: LogWisp still terminates TLS.
- nginx side:
- In the `stream` server, `proxy_protocol on` on the route to LogWisp (it
sends v1).
- HAProxy: `send-proxy` or `send-proxy-v2`.
- Relation to `auth.trusted_proxies` (HTTP proxy mode):
- That list trusts `X-Forwarded-For` from a TLS-terminating L7 proxy;
`proxy_from` trusts a PROXY header from an L4 proxy.
- Keep both, with distinct names and docs. They answer different layers, and
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
- `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`.
### 1.4 Per-client limits
- Generalize the SCRAM limiter in `internal/authz/scram.go` (token bucket per
key, bounded table that fails closed, idle sweep, IPv6 per /64 via
`throttleKey`) into the shared per-key limiter. One mechanism (AGENTS.md):
SCRAM throttling then uses it too.
- Keys:
- `max_connections_per_client` on every listener (concurrent connections per
address).
- `requests_per_second_per_client` on HTTP listeners (ingest, stream and
status requests).
- Later: per-identity limits once a peer is authenticated (the mtls plan's
deferred item 2), keyed by identity instead of address.
### 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
each refusal.
### 1.6 Verification
- Go tests, one per rule:
- 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`:
- 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.
## 2. Packaging: AUR, FreeBSD ports, Debian
The foundation exists:
- The `lw` binary name, free in Arch (official repositories and AUR), Ubuntu
24.04 and 26.04, and the FreeBSD 15.1 ports tree.
- The canonical module path `github.com/lixenwraith/logwisp`, which
`go install`, FreeBSD's `USES=go:modules` and Debian's dh-golang expect.
- `make install` with `DESTDIR`, `PREFIX` and `SYSCONFDIR`.
- The `doc/lw.1` manual.
- Service files in `deploy/package/`: systemd unit, sysusers, tmpfiles and the
FreeBSD rc.d script.
- Skeletons in `deploy/package/arch/` and `deploy/package/freebsd/`.
What remains. Steps 1 to 3 are the maintainer's: they need push rights and an
identity, and both skeletons download the tagged source.
1. Tag the release on the merged main commit, `vX.Y.Z`:
```
git tag -a vX.Y.Z -m vX.Y.Z # git tag -s signs it with your key
git push origin vX.Y.Z
```
- `make` then stamps `vX.Y.Z` (from `git describe`), and
`go install github.com/lixenwraith/logwisp/cmd/lw@vX.Y.Z` works.
- A GitHub release for the tag is optional; the tag alone serves
`archive/vX.Y.Z.tar.gz`, which the PKGBUILD downloads.
2. Arch (AUR), `deploy/package/arch/PKGBUILD`:
- `pkgver=X.Y.Z` and the `# Maintainer: Name <email>` line.
- `updpkgsums` (pacman-contrib) replaces `sha256sums=('SKIP')` with the
tarball's checksum.
- `namcap PKGBUILD`, then `makepkg -si` (builds, runs `check()`,
installs), then `makepkg --printsrcinfo > .SRCINFO`.
- Push `PKGBUILD` and `.SRCINFO` to
`ssh://aur@aur.archlinux.org/logwisp.git`; optionally a `logwisp-git`
package built from the main branch.
3. FreeBSD, `deploy/package/freebsd/` (`sysutils/logwisp`):
- `DISTVERSION=X.Y.Z` and `MAINTAINER=` your address.
- `make makesum` writes `distinfo`: the module zip that the Go proxy
serves for `GO_MODULE` at the tag.
- The `logwisp` user needs an ID registered in the ports tree. Take a
number below 1000 that is free in both `/usr/ports/UIDs` and
`/usr/ports/GIDs`, and add to the same patch:
```
UIDs: logwisp:*:NNN:NNN::0:0:LogWisp daemon:/nonexistent:/usr/sbin/nologin
GIDs: logwisp:*:NNN:
```
- `portlint -AC`, `poudriere testport` on 14.x and 15.x jails, then a
Bugzilla report with the port directory and the UIDs/GIDs diff.
4. Debian:
- Debian policy wants every Go dependency packaged; the four `lixenwraith`
libraries are not.
- Either package them too (dh-golang), or start with an `.deb` built by the
Makefile `install` target (nfpm or `dpkg-deb`) and a PPA, and move to the
archive later.
5. Shell completion for bash, zsh and fish, installed by `make install`.
6. A packaging CI job: build the AUR package in an Arch container and the port
in a FreeBSD VM, run `lw --version` and `make image-check`.