Commit graph

3 commits

Author SHA1 Message Date
mithral
795b77b709 Advertise Vellum over mDNS so clients find it without a typed URL
internal/discovery registers three service types on the listen port:
_vellum._tcp for native clients, and _jellyfin._tcp / _subsonic._tcp
because those ecosystems already have clients that browse the LAN. That
last part is the point: Infuse, Jellyfin Media Player and Symfonium can
find Vellum with no client-side work, because they are already looking for
those names.

TXT records carry api, ver, proto, types, tls and (when set) pub, so a
client that resolves one can tell what it found before connecting.

Definitions is a pure function of config and Start only registers what it
returns, so what is advertised is testable without a socket.

Two behaviours worth knowing:
- VELLUM_DISCOVERY=off stops advertising. It accepts off/no/false/0;
  strconv.ParseBool would have silently ignored "off".
- A loopback-only listen address advertises nothing. That is the normal
  shape behind a reverse proxy, and advertising it would put a port in the
  LAN browse list that refuses every client that dials it. Verified
  red-green: defeating the guard fails TestDefinitionsSkipsLoopbackListen.

zeroconf is the only new dependency. It logs to the stdlib logger, so a
host with no multicast-capable interface prints to stderr outside slog.

Not verified: real mDNS on a real LAN. There is no multicast here, so the
wire behaviour of zeroconf is untested - only the definitions it is handed.
2026-10-12 01:04:33 -04:00
mithral
87c17d4854 Make the login limiter correct behind a reverse proxy
Some checks failed
CI / ci (push) Has been cancelled
ClientIP used r.RemoteAddr and ignored X-Forwarded-For. That was right
about not trusting the header from an untrusted peer, but it is only half
the design: behind a proxy every request arrives from the proxy, so the
limiter key in handlers.go was "<proxy-ip>|<username>" for every client on
the internet. One attacker locked out a username for everyone, an attacker
rotating usernames never hit a limit, and sessions.ip recorded the proxy
so the device list was useless.

- VELLUM_TRUSTED_PROXIES is a CIDR list. X-Forwarded-For is honoured only
  when RemoteAddr is inside it, and the chain is walked right-to-left so
  the first address that is not itself a trusted proxy wins. Everything to
  its left was appended by a client we do not control, so a spoofed
  leftmost entry is discarded.
- VELLUM_PUBLIC_URL is the external origin. CookieSecure now defaults to
  true when it is https://, so a TLS deployment that forgets
  VELLUM_COOKIE_SECURE no longer ships a plaintext session cookie.
- A malformed CIDR fails the boot rather than being skipped: silently
  dropping a proxy network widens the trust boundary, which is the exact
  failure this setting exists to prevent.

ClientIP became a method on Authenticator so it can see the trusted set,
which is why handlers.go and main.go changed as well.

Verified red-green: neutering the trusted-peer path fails five ClientIP
tests, including the spoofed-leftmost case.

Known gap, pre-existing and not fixed here: this limiter still covers only
/api/v1/auth/login. Jellyfin AuthenticateByName and Subsonic auth call
auth.Authenticate with no limiting, so the same brute force is available
through those two surfaces.
2026-10-12 00:45:27 -04:00
mithral
eb1ad6d920 Phase 0: skeleton, config, migrations, health check, container
Bootstraps Vellum as a single Go binary with one SQLite database.

- core: env-driven config, slog logging, SQLite (WAL) with pooled pragmas,
  embedded migration runner keyed on PRAGMA user_version
- migrations/0001_core.sql: identity, media, user state, infra. Media items
  are global; all per-user state (progress, favorites, grants) is a join.
  One polymorphic progress table covers every media kind.
- httpapi: request-ID, structured request logging, panic recovery, JSON
  endpoints for /healthz and /api/v1/version
- multi-arch Dockerfile (pure-Go SQLite, CGO_ENABLED=0) with an entrypoint
  that honours PUID/PGID like the rest of the self-hosted stack
- tests for migration idempotency, FK enforcement, WAL, config, middleware

Media does not exist yet. Nothing is exposed beyond health and version.
2026-10-09 16:26:08 -04:00