protocol

package
v1.99.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 20, 2026 License: GPL-3.0 Imports: 21 Imported by: 0

Documentation

Overview

Package protocol implements the yarilo-auth TCP+mTLS protocol. Server → Client handshake, then AUTH/CONT/CANCEL commands.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func AssignField added in v1.39.0

func AssignField(info *UserInfo, key, value string) error

AssignField populates a single field on UserInfo from its canonical name + string value. Used by every layer that reconstructs a UserInfo from a string-keyed source: the SQL userdb driver (SELECT row column names) and pkg/authclient (`key=value` wire pairs from the master-protocol response).

Unknown keys land in UserInfo.Extra so a richer source schema does not lose data; `forward_*` keys auto-populate UserInfo.Forward. Boolean fields accept the canonical truthy values (1 / yes / y / true / t / on, case-insensitive); list fields (Groups, QuotaRules, AllowNets) accept comma-separated values and append, so a caller may invoke AssignField multiple times with the same key to merge entries (Dovecot allows `quota_rule=` to appear repeatedly).

func IsTruthy added in v1.39.0

func IsTruthy(s string) bool

IsTruthy reports whether s is one of the canonical truthy values auth-fields columns / wire pairs use (`1`, `yes`, `y`, `true`, `t`, `on`, case-insensitive). Anything else returns false — callers do not need to special-case empty strings or unknown values.

func MakeCacheKey added in v1.39.0

func MakeCacheKey(service, username string) string

MakeCacheKey assembles the canonical cache key for a passdb / userdb lookup. Pinned to a single shape across drivers for predictability. Wire layer + chainAuthenticator both call this so cache hits are interchangeable between in-process and remote auth paths for the same (user, service).

func ParseFieldPair added in v1.39.0

func ParseFieldPair(token string) (key, value string)

ParseFieldPair splits one `key=value` token from the master-protocol wire into (key, value), undoing the escapes marshalUserInfo applies for TAB / LF / NUL / backslash. Tokens without `=` are treated as bare flags with empty value (mirroring Dovecot — `nodelay` on its own is equivalent to `nodelay=yes`).

func SplitCSV added in v1.39.0

func SplitCSV(s string) []string

SplitCSV splits a comma-separated value, trims whitespace around each entry, and drops empty entries. Returns nil for an empty or whitespace-only input so callers can assign the result directly to a `[]string` field without churning a slice the consumer must then filter.

func SplitMasterFromAuthid added in v1.39.0

func SplitMasterFromAuthid(authid, sep string) (master, target string)

SplitMasterFromAuthid extracts a (master, target) pair from an authid that uses the `target<sep>master` workaround for clients that cannot supply authzid in the PLAIN response (older Outlook, some mobile clients). Returns ("", authid) when sep is empty or not found — authid passes through unchanged.

A SASL response of the form `\0target*master\0password` is equivalent to the standards-compliant `target\0master\0password`.

Types

type AuthResponse

type AuthResponse struct {
	Result   AuthResult
	Username string
	Home     string
	MailLoc  string
	// Groups is the list of supplementary groups the user belongs to,
	// sourced from the userdb `groups=` extra field. Used by ACL
	// evaluation to match `group=` and `group-override=` entries.
	// Empty when not configured — group= ACL entries have no effect.
	Groups []string

	// QuotaRules is the list of per-user quota rules sourced from the
	// userdb `quota_rule=` extra field. Format: `*:storage=5G`.
	QuotaRules []string

	// VolatileDir is the VOLATILEDIR modifier from the mail location (or
	// direct volatile_dir= userdb field). Carries the raw template string
	// (%u/%n/%d/%h unexpanded) — callers expand it against the resolved
	// home after auth completes.
	VolatileDir string

	// IndexDir is the INDEX= modifier from the mail location (or direct
	// index_dir= userdb field). Carries the raw template string
	// (%u/%n/%d/%h unexpanded). When set, per-folder index files live
	// here instead of co-located with the mailbox under Home.
	IndexDir string

	// ControlDir is the CONTROL= modifier from the mail location (or direct
	// control_dir= userdb field). Carries the raw template string
	// (%u/%n/%d/%h unexpanded). When set, per-folder control files
	// (yarilo-uidlist, subscriptions) live here instead of co-located
	// with the mailbox under Home.
	ControlDir string

	// AltDir is the ALT= modifier from the mail location (or direct
	// alt_dir= userdb field). Carries the raw template string
	// (%u/%n/%d/%h unexpanded). When set, cold-tiered messages live
	// under AltDir; reads check both primary and alt tiers.
	AltDir string

	Proxy bool
	Host  string
	Port  int

	// Fields carries the passdb result as a key/value bag with
	// prefix-derived scoping (see fields.go). Populated by passdb
	// implementations that opt into Phase AUTH-2's wire surface;
	// nil falls back to the typed-fields wire path.
	Fields *Fields
}

AuthResponse is the final response sent back to the client session.

Phase AUTH-2 PR 1 adds Fields alongside the legacy typed members. When a passdb populates Fields, handleAuth emits the OK reply from the bag (with prefix gating: auth_* stripped, userdb_* passed through). When Fields is nil the typed fallback path runs — that's the pre-AUTH-2 wire shape, byte-compatible for the existing SQL passdb until PR 2 swaps the Passdb interface to take a shared Fields instance directly.

type AuthResult

type AuthResult int

AuthResult is the outcome of a single authentication attempt.

const (
	AuthOK AuthResult = iota
	AuthFail
	AuthTempFail
)

type Authenticator added in v1.39.0

type Authenticator interface {
	Authenticate(username, password, service, remoteIP string) (*AuthResponse, error)
}

Authenticator is the simpler legacy surface session paths (yarilo-imap, yarilo-pop3, etc.) use directly when they need to verify credentials in-process (without going over the yarilo-auth wire). Wraps a Passdb chain and projects the chain- internal Result back onto a wire-shaped AuthResponse so the caller does not need to know about Request / Result.

func NewAuthenticator added in v1.39.0

func NewAuthenticator(passdbs []Passdb, opts ...AuthenticatorOption) Authenticator

NewAuthenticator wraps one or more Passdb drivers into the session-friendly Authenticator surface. Passdb-only: userdb lookups are the session process's responsibility (via the master protocol).

Master-user impersonation is opt-in via WithAuthenticatorMasterUsers(true). When disabled (the default) the returned Authenticator deliberately does NOT implement MasterAuthenticator — so session-level type assertions in IMAP/POP3/Submission fail and any distinct SASL PLAIN authzid is rejected. Operators must explicitly flip the toggle (and configure masterdb / separator) to enable the feature.

type AuthenticatorOption added in v1.39.0

type AuthenticatorOption func(*chainAuthenticator)

AuthenticatorOption tunes a NewAuthenticator construction.

func WithAuthenticatorCache added in v1.39.0

func WithAuthenticatorCache(c *Cache) AuthenticatorOption

WithAuthenticatorCache attaches an auth cache. When set, every Authenticate / AuthenticateMaster call first consults the cache (key = `<service>\t<username>`) and short-circuits on hit. Misses run the full chain and seed the cache with the result. Nil cache = caching disabled (every call goes to the chain).

The cache is keyed by the passdb-side username; in a master-user flow the master and target produce independent cache entries.

func WithAuthenticatorMasterUserSeparator added in v1.39.0

func WithAuthenticatorMasterUserSeparator(sep string) AuthenticatorOption

WithAuthenticatorMasterUserSeparator enables the `target<sep>master` SASL workaround for legacy clients that cannot send authzid. Empty disables it. Mirrors WithMasterUserSeparator on the wire Server.

func WithAuthenticatorMasterUsers added in v1.39.0

func WithAuthenticatorMasterUsers(enabled bool) AuthenticatorOption

WithAuthenticatorMasterUsers flips the top-level master-user opt-in. When false (the default), NewAuthenticator returns an Authenticator-only wrapper — type-asserts to MasterAuthenticator at every protocol entry point fail, so any distinct SASL PLAIN authzid is rejected before the chain is consulted. When true, the chainAuthenticator's AuthenticateMaster method is exposed and the masterdb / separator options take effect.

func WithAuthenticatorMasterdb added in v1.39.0

func WithAuthenticatorMasterdb(passdbs []Passdb) AuthenticatorOption

WithAuthenticatorMasterdb attaches a dedicated masterdb chain. Mirrors WithMasterdb on the wire Server: AuthenticateMaster consults this chain first, falling through to the main passdb's per-user `master_user=yes` flag only when no masterdb entry knows the master.

type Cache added in v1.39.0

type Cache struct {
	// contains filtered or unexported fields
}

Cache is an in-process LRU cache for verified auth lookups.

  • Bytes-bounded: SizeBytes caps the total payload weight; LRU eviction makes room when an insert would exceed the cap.
  • Two TTLs: positive entries (successful auth) live up to TTL, negative entries (failed lookup — unknown user, wrong password) live up to NegTTL. A separate, typically shorter NegTTL means a fresh password attempt can succeed soon after the user resets their password without waiting for the full positive TTL to expire.
  • Password verify on hit: the cached entry stores an HMAC of the plain password (computed with a process-local random key). On lookup, the incoming password is HMAC-ed and compared in constant time. Negative entries store an empty password slot so any incoming password triggers re-lookup against the chain if the user happens to exist now.

Caching defence-in-depth: the HMAC key is generated per Cache construction and never persisted. A memory dump reveals the HMACs (which are not invertible) but not plain passwords. The key never crosses any wire — even the master-protocol CACHE-FLUSH only carries user masks, not entries.

Cache key format is decided by the caller (typically `service\tusername`); Cache treats keys as opaque.

func NewCache added in v1.39.0

func NewCache(sizeBytes int64, ttl, negTTL time.Duration) *Cache

NewCache returns a Cache bounded by sizeBytes total payload. A non-positive sizeBytes returns nil — caching is disabled and every helper that takes a *Cache no-ops on nil.

ttl is the positive-entry lifetime; negTTL is the negative-entry lifetime. ttl=0 disables positive caching; negTTL=0 disables negative caching. Setting ttl=0 with sizeBytes>0 means only negatives are cached (and vice versa).

func (*Cache) Clear added in v1.39.0

func (c *Cache) Clear() uint32

Clear evicts every entry. Returns how many were removed.

func (*Cache) ClearByUserMask added in v1.39.0

func (c *Cache) ClearByUserMask(masks []string) uint32

ClearByUserMask evicts every entry whose stored username matches any of the supplied masks. Mask syntax: `*` matches zero or more chars, `?` matches one (admin CLI: `yarilo-admin auth cache flush [<user-mask>...]`).

Empty masks slice → behaves like Clear (full flush).

func (*Cache) Insert added in v1.39.0

func (c *Cache) Insert(key, username, password string, result Result, fields *Fields)

Insert stores an entry under key. password is the plain credential (HMAC-ed before storage; never retained as plain). For negative entries pass an empty password — Insert stores no pwdMAC and Lookup treats every Fail entry as an unconditional short-circuit regardless of incoming password.

fields is the AuthResponse bag captured at the chain's reply; it's stored by reference so the caller must not mutate it after Insert returns. Pass a snapshot (Fields.Snapshot then a fresh Fields rebuilt) if mutation is unavoidable.

func (*Cache) Lookup added in v1.39.0

func (c *Cache) Lookup(key, password string) (*CacheEntry, bool)

Lookup checks the cache for key. On positive hit it verifies the incoming password against the stored HMAC in constant time; mismatch is treated as miss so a stale cached password cannot authenticate a new one. Negative hits return (entry, true) with Result=ResultFail and the caller short-circuits without touching the chain.

Expired entries are evicted lazily — Lookup removes them on touch so capacity accounting stays current. neg_expired_r equivalent: when an expired entry is encountered the caller gets (nil, false) and runs the chain; the eviction means a reinsert can land cleanly.

Returns (nil, false) for cache miss or password mismatch.

func (*Cache) Remove added in v1.39.0

func (c *Cache) Remove(key string) bool

Remove deletes a single key. Returns true if an entry existed.

func (*Cache) Stats added in v1.39.0

func (c *Cache) Stats() (hits, misses uint64, sizeBytes, entryCount int64)

Stats returns hit / miss / size counters for telemetry. The returned size is the byte estimate, not entry count.

type CacheEntry added in v1.39.0

type CacheEntry struct {
	Result Result
	Fields *Fields
	// contains filtered or unexported fields
}

CacheEntry is the cached value. ResultOK carries Fields (the passdb result the chain produced) and pwdMAC (HMAC of the verified plain password). ResultFail carries neither — the presence of a Fail entry alone short-circuits the chain.

pwdMAC is set only on positive entries. Empty pwdMAC on a positive entry would let any caller pass on a cache hit, so callers MUST treat (Result=OK, len(pwdMAC)=0) as cache miss.

type Chain

type Chain []Passdb

Chain composes multiple Passdb backends with first-hit-wins semantics. Each entry runs under a per-entry Fields.Snapshot so a driver's partial mutations are rolled back on ResultNext. Wire surface preserved from pre-PR-2: chain exhaust without a hit returns ResultFail (matches the pre-refactor Chain that returned AuthFail when every driver answered nil).

func (Chain) Authenticate

func (c Chain) Authenticate(req *Request) (Result, error)

Authenticate walks the chain. Snapshot before each driver, rollback on ResultNext, propagate immediately on OK / Fail / TempFail. Allocates the Fields bag on demand when the caller passed nil so drivers can always mutate without a nil-check.

type Fields added in v1.39.0

type Fields struct {
	// contains filtered or unexported fields
}

Fields is an ordered key/value bag that carries the result of a passdb (and, eventually, userdb) lookup through the auth pipeline. Insertion order is preserved so the wire output is deterministic for any given mutation sequence — two processes that build the same Fields produce byte-identical OK responses.

Scope is derived from the key prefix at iteration / serialise time, not stored on the entry:

auth_*    → ScopeInternal — never crosses the client wire
userdb_*  → ScopeUserdb   — passed through with the prefix
                            preserved so login pods can tell
                            them apart from passdb-only fields
(other)   → ScopePassdb   — passdb-only field, emitted on the
                            wire verbatim

Phase AUTH-2 PR 1 introduces Fields alongside the legacy typed AuthResponse fields. Both populate in parallel so the existing SQL passdb + handleAuth call sites stay byte-compatible with the pre-AUTH-2 wire. PR 2 swaps the Passdb interface to take a shared Fields instance so chains can mutate it; PR 3 wires userdb prefetch through the same bag.

func NewFields added in v1.39.0

func NewFields() *Fields

NewFields constructs an empty bag.

func (*Fields) Delete added in v1.39.0

func (f *Fields) Delete(key string)

Delete removes a key. No-op when the key is absent. Subsequent keys shift to fill the gap so iteration order stays sequential.

func (*Fields) Each added in v1.39.0

func (f *Fields) Each(fn func(key, value string) bool)

Each iterates in insertion order. The visitor returns false to stop early; true to continue. nil bag yields nothing.

func (*Fields) Get added in v1.39.0

func (f *Fields) Get(key string) (string, bool)

Get returns (value, true) when the key is present.

func (*Fields) Has added in v1.39.0

func (f *Fields) Has(key string) bool

Has reports whether the key is set.

func (*Fields) Len added in v1.39.0

func (f *Fields) Len() int

Len returns the number of fields. Returns 0 for a nil bag so callers do not have to nil-check before length-based decisions.

func (*Fields) Rollback added in v1.39.0

func (f *Fields) Rollback(snap *Snapshot)

Rollback restores the bag to the state captured by snap. Calling Rollback with a nil snap is a no-op so callers do not need to branch on Snapshot returning nil.

func (*Fields) Set added in v1.39.0

func (f *Fields) Set(key, value string)

Set adds or overwrites a key. Stable: rewriting a key keeps its original insertion-order position so the wire serialisation does not flip mid-stream.

func (*Fields) SetValidated added in v1.39.0

func (f *Fields) SetValidated(key, value string) error

SetValidated parses value through the reserved-field registry before storing. When key matches a known reserved name (or `userdb_<base>` where base is reserved), the validator returns the canonical form and that goes into the bag. Unknown keys (including everything `forward_*`-prefixed and anything not in the registry) pass through verbatim — same behaviour as Set.

Returns a non-nil error when validation fails; the bag is NOT mutated in that case so callers can retry / surface the parse error without rolling back. Intended for admin / wire-side callers that ingest arbitrary input; driver-side passdb code keeps using Set (the driver knows its own schema).

func (*Fields) Snapshot added in v1.39.0

func (f *Fields) Snapshot() *Snapshot

Snapshot captures the bag's state. Returns nil for a nil bag (Rollback is also nil-safe) so callers can use the pair without pre-allocation when the bag is optional.

func (*Fields) WireForm added in v1.39.0

func (f *Fields) WireForm() []string

WireForm renders Fields as the tab-delimited `key=value` sequence the client protocol's OK response carries. ScopeInternal entries are dropped unconditionally; ScopeUserdb and ScopePassdb entries are emitted in insertion order with values escape-encoded so embedded tabs / newlines / NULs cannot break the wire framing.

The first element returned is the response's user= token when `user=` is present in the bag — the rest of the auth pipeline expects the username token to lead. Callers that surface username via a different mechanism may pass it explicitly and drop the `user=` entry before serialising.

type MasterAuthenticator added in v1.39.0

type MasterAuthenticator interface {
	AuthenticateMaster(authzid, authid, password, service, remoteIP string) (*AuthResponse, error)
}

MasterAuthenticator extends Authenticator with the SASL PLAIN authzid surface — i.e. master-user impersonation. Session-level callers (IMAP / POP3 / Submission) type-assert opts.Auth into this interface to decide whether to honour authzid; backends that only implement Authenticator (or stub test doubles) keep working unchanged but fall back to "authzid must equal authid" behaviour at the SASL layer.

  • authzid — target identity the caller wants to log in AS. When empty (or equal to authid), this is a regular login and callers should typically dispatch via Authenticate instead.
  • authid — the user supplying the password (the master in an impersonation request).
  • password — the master's password.
  • service — login service tag (imap / pop3 / submission / lmtp). Logged + forwarded to the chain unmodified.

type MasterServer added in v1.39.0

type MasterServer struct {
	// contains filtered or unexported fields
}

MasterServer answers password-less userdb lookups and user enumeration over a separate TCP+mTLS listener. Wire-format mirrors Dovecot's auth-master-connection at the shape level (USER / PASS / LIST commands with ID-tracked responses) so admin tooling that already knows the Dovecot dialect maps over without surprises.

MasterServer is intentionally separate from the client-protocol Server: different audience (admins + backend-api vs login pods), different threat model (no untrusted credentials cross this socket), and different evolution cadence. The two share mTLS material today; splitting is a future operational knob.

func NewMasterServer added in v1.39.0

func NewMasterServer(userdb Userdb, opts ...MasterServerOption) *MasterServer

NewMasterServer constructs a MasterServer rooted at the given userdb. The userdb may be a UserdbChain composing multiple backends; LIST opportunistically uses UserdbIterator via type assertion when the supplied userdb implements it.

Passing a nil userdb is legal — every USER lookup will respond NOTFOUND, LIST will respond FAIL. Useful for deployments that expose only the wire surface (e.g. for connectivity checks) without configured backends.

func (*MasterServer) ListenAndServe added in v1.39.0

func (s *MasterServer) ListenAndServe(ctx context.Context, addr string, tlsCfg *tls.Config) error

ListenAndServe accepts connections until ctx is cancelled. When tlsCfg is non-nil the listener uses mTLS (TLS 1.3, RequireAndVerifyClientCert is the caller's responsibility — set it on tlsCfg). Each connection is served by its own goroutine; active conns drain before the function returns.

type MasterServerOption added in v1.39.0

type MasterServerOption func(*MasterServer)

MasterServerOption tunes a NewMasterServer construction.

func WithMasterCache added in v1.39.0

func WithMasterCache(c *Cache) MasterServerOption

WithMasterCache attaches the auth cache that the master protocol's CACHE-FLUSH verb operates on. Pass the same *Cache instance the client-protocol Server received via WithCache so flushing through the admin socket actually empties the cache servicing login pods. Nil cache means CACHE-FLUSH responds FAIL.

func WithMasterTokenStore added in v1.57.0

func WithMasterTokenStore(ts TokenStore) MasterServerOption

WithMasterTokenStore attaches the shared token store. When set, the SESSION command issues tokens that the client-protocol VERIFY command can validate (both Server and MasterServer must receive the same TokenStore instance). When nil, SESSION responds FAIL.

type Passdb

type Passdb interface {
	Authenticate(req *Request) (Result, error)
}

Passdb is the interface that passdb backends implement. Drivers receive a chain-wide Request whose Fields bag they mutate directly; the returned Result selects the chain's next move. error is reserved for unexpected technical failures that accompany ResultTempFail — the wire never carries the error text, only the temp_fail marker, so a driver-level "connection refused" is invisible to the login pod.

type PenaltyStore added in v1.39.0

type PenaltyStore interface {
	PenaltyLookup(ip string) (int, error)
	PenaltyUpdate(ip string, count int) error
}

PenaltyStore is the cross-process auth-fail backoff store the wire Server consults pre-passdb. Implemented by *anvil.Conn so every yarilo-auth pod shares the same counters; tests can plug in a stub. Nil-safe at the call sites — Lookup returns 0 and Update is a no-op when the store is not configured.

type PenaltyToSecsFunc added in v1.39.0

type PenaltyToSecsFunc func(count int) int

PenaltyToSecsFunc maps the penalty counter returned by PenaltyStore.Lookup to the sleep duration applied before the chain runs. Caller-supplied so a deployment can tune the exponential curve without recompiling protocol — typical implementations: anvil.PenaltyToSecs (2/4/8/15 cap), linear, or capped-linear. Nil falls back to no-sleep.

type PolicyChecker added in v1.39.0

type PolicyChecker interface {
	CheckBefore(ctx context.Context, req PolicyRequest) (PolicyDecision, error)
	CheckAfter(ctx context.Context, req PolicyRequest, success, policyReject bool) (PolicyDecision, error)
	ReportAfter(ctx context.Context, req PolicyRequest, success, policyReject bool)
}

PolicyChecker is the policy-server hook surface the wire Server consults around the chain run. Implemented by *policy.Client (auth/policy package); tests can plug stubs.

Decision shape lives here so protocol does not import the policy package — single-direction dependency keeps the build graph clean.

type PolicyDecision added in v1.39.0

type PolicyDecision struct {
	Continue   bool
	Reject     bool
	TarpitSecs int
	Message    string
}

PolicyDecision is the parsed verdict. Continue=true means proceed; Reject=true means refuse; TarpitSecs>0 means sleep that many seconds then continue.

type PolicyMode added in v1.39.0

type PolicyMode struct {
	CheckBefore bool
	CheckAfter  bool
	ReportAfter bool
}

PolicyMode controls which lifecycle hooks fire. CheckBefore runs pre-passdb (block the chain on policy refusal); CheckAfter runs post-passdb with the outcome known (downgrade-success-to-fail use case); ReportAfter is fire-and-forget telemetry.

type PolicyRequest added in v1.39.0

type PolicyRequest struct {
	Username  string
	Password  string
	RemoteIP  string
	Service   string
	DeviceID  string
	SessionID string
	TLS       bool
	FailType  string
}

PolicyRequest is the input to a policy call. Mirror of the shape policy.Client accepts; defined here so the wire Server can call without the policy package import.

type Request added in v1.39.0

type Request struct {
	Username string
	Password string
	Service  string
	// RemoteIP is the client IP string (no port). Empty when the IP is
	// unavailable (in-process chain, loopback test harnesses). Passdb
	// drivers that enforce allow_nets MUST skip the check when empty.
	RemoteIP string

	// Fields is the chain-wide bag that accumulates passdb output.
	// Driver implementations MAY assume non-nil; the canonical
	// caller (Chain.Authenticate) allocates it when the Server-side
	// authenticate entry-point starts the chain.
	Fields *Fields
}

Request bundles every input a passdb needs to authenticate plus the shared Fields bag the chain mutates as it walks. Phase AUTH-2 PR 2 introduces this struct so passdb backends can write directly into the bag (the chain isolates each driver's mutations via Snapshot / Rollback on ResultNext). PR 3 will extend Request with userdb-prefetch metadata.

type Result added in v1.39.0

type Result int

Result classifies a passdb outcome. Separate from AuthResult (which is wire-shaped on AuthResponse) because ResultNext is a chain-internal signal — it never reaches the client wire.

const (
	// ResultOK — user verified, Fields populated, chain stops.
	ResultOK Result = iota
	// ResultFail — credentials rejected (password mismatch,
	// disabled account, expired credential). Chain stops; the
	// wire returns FAIL.
	ResultFail
	// ResultNext — user unknown in this driver; chain rolls back
	// any partial Fields mutations and tries the next driver.
	ResultNext
	// ResultTempFail — backend technical failure (DB down,
	// network). Chain stops with a wire-level FAIL temp_fail.
	// Always accompanies a non-nil error so the server-side log
	// has the underlying cause.
	ResultTempFail
)

func RunAuth added in v1.39.0

func RunAuth(chain Chain, req *Request) (Result, error)

RunAuth executes the passdb chain. passdb is authoritative for the auth decision; userdb is never called here. Session processes obtain home/mail/quota via a separate USER command on the master socket after a successful VERIFY.

func RunMasterAuth added in v1.39.0

func RunMasterAuth(passdbs, masterdb Chain, target string, req *Request) (Result, error)

hasUserdbFields reports whether the bag already carries at least one userdb_-prefixed key — the marker for a prefetched userdb result that a downstream userdb.Lookup must not overwrite. RunMasterAuth handles a master-user impersonation request. req.Username MUST be the master when the function is called. On success req.Fields carries master_user/original_user/user. userdb is not consulted here — session processes call it via the master socket after VERIFY.

type SCRAMSha1Lookup added in v1.39.0

type SCRAMSha1Lookup interface {
	LookupSCRAMSha1(username string) (*sasl.ScramCredentials, error)
}

SCRAMSha1Lookup is the SHA-1 counterpart of SCRAMSha256Lookup. Same semantics, same (creds, nil) / (nil, nil) / (nil, err) contract; only the digest family of the returned verifier differs. Provided for compatibility with legacy clients that only speak SCRAM-SHA-1 — new deployments should provision SCRAM-SHA-256 verifiers instead.

type SCRAMSha256Lookup added in v1.39.0

type SCRAMSha256Lookup interface {
	LookupSCRAMSha256(username string) (*sasl.ScramCredentials, error)
}

SCRAMSha256Lookup exposes per-user SCRAM-SHA-256 verifiers to the session-layer SASL mech. Implementing this interface on the configured Authenticator (and on individual passdbs that carry verifiers in their backing store) lights up SCRAM-SHA-256 and SCRAM-SHA-256-PLUS advertisement in IMAP / POP3 / Submission.

LookupSCRAMSha256 returns:

  • (creds, nil) — user exists and has a SCRAM-SHA-256 verifier. The SASL mech drives challenge-response from the returned StoredKey / ServerKey without ever seeing a plain password.
  • (nil, nil) — user unknown OR stored credential is not a SCRAM verifier. The SASL mech fabricates a fake verifier so the exchange completes with a uniform auth-failed outcome and an attacker cannot probe for user existence via timing.
  • (nil, err) — transient backend error. The session surfaces this as temp_fail.

Verifiers are produced by `yarilo-admin auth scram-verifier` (or any tool that emits the `{SCRAM-SHA-256}iter,salt,stored,server` blob).

type Scope added in v1.39.0

type Scope int

Scope classifies a field by its key prefix. Defined as a type so switch statements stay exhaustive when phase AUTH-2 PR 2 adds snapshot tracking.

const (
	// ScopePassdb — passdb-side fields (no prefix). Crosses the
	// client wire as `key=value`.
	ScopePassdb Scope = iota
	// ScopeUserdb — fields prefetched by passdb for the userdb
	// layer. Crosses the client wire as `userdb_key=value` so
	// login pods can split the response into passdb / userdb
	// halves the way Dovecot's auth-client does.
	ScopeUserdb
	// ScopeInternal — internal-only metadata. Never crosses the
	// client wire. Used for state-tracking fields like
	// `auth_cache_key`, `auth_failure_attempted`, etc. in later
	// AUTH-N phases.
	ScopeInternal
)

func ScopeOf added in v1.39.0

func ScopeOf(key string) Scope

ScopeOf returns the scope a key belongs to. The classification is derived from the prefix so callers do not need to remember which flags to set when calling Set.

type Server

type Server struct {
	// contains filtered or unexported fields
}

Server is the yarilo-auth TCP+mTLS server. Holds the passdb chain plus an optional userdb that enriches successful auth responses with userdb_* fields (Phase AUTH-2 PR 3) plus an optional masterdb chain + separator for master-user impersonation (Phase AUTH-3).

func NewServer

func NewServer(passdbs []Passdb, opts ...ServerOption) *Server

NewServer creates an auth server with the given passdb chain. Optional userdb is attached via WithUserdb.

func (*Server) ListenAndServe

func (s *Server) ListenAndServe(ctx context.Context, addr string, tlsCfg *tls.Config) error

ListenAndServe starts the auth TCP server. When tlsCfg is non-nil the listener uses mTLS (TLS 1.3, RequireAndVerifyClientCert). Blocks until ctx is cancelled; active sessions drain before the function returns.

type ServerOption added in v1.39.0

type ServerOption func(*Server)

ServerOption tunes a NewServer construction.

func WithCache added in v1.39.0

func WithCache(c *Cache) ServerOption

WithCache attaches an auth cache. handleAuth consults it before running the passdb chain — positive hit verifies the supplied password against the stored HMAC, negative hit short-circuits without password check. Misses run the chain and seed the cache with the result.

Nil cache disables caching. Pass the same *Cache instance to WithMasterCache on the matching MasterServer to expose CACHE-FLUSH for selective eviction.

func WithFailureDelay added in v1.39.0

func WithFailureDelay(d time.Duration) ServerOption

WithFailureDelay sets the timing-leak mitigation delay for client-visible auth failures (wrong password, unknown user, malformed SASL). The reply is held back by d before writing to the wire. Zero disables the delay (mostly for tests).

func WithInternalFailureDelay added in v1.39.0

func WithInternalFailureDelay(d time.Duration) ServerOption

WithInternalFailureDelay sets the delay applied when the failure is internal (passdb backend down, SQL refused, etc). Separate from WithFailureDelay because operators often want internal-error delays shorter than user-facing ones (the user will likely retry and another full failure delay would amplify outage symptoms).

func WithMasterUserSeparator added in v1.39.0

func WithMasterUserSeparator(sep string) ServerOption

WithMasterUserSeparator enables the `target<sep>master` workaround for SASL PLAIN clients that cannot supply authzid in the standard third-field position. Typical value is `*`. Empty disables the workaround — only RFC 4616 authzid is honoured.

func WithMasterUsers added in v1.39.0

func WithMasterUsers(enabled bool) ServerOption

WithMasterUsers is the top-level opt-in for master-user impersonation on the wire AUTH path. While false (the default) handleAuth ignores any SASL PLAIN authzid the client sends AND skips the separator workaround — every request routes through the regular passdb chain, indistinguishable from a build without master support. Set to true to expose the master flow at the wire level.

func WithMasterdb added in v1.39.0

func WithMasterdb(passdbs []Passdb) ServerOption

WithMasterdb attaches a dedicated masterdb chain. Master-user impersonation requests (PLAIN SASL responses where authzid is set and differs from authid) authenticate the authid against the masterdb first; if absent or not configured, the request falls through to the regular passdb and looks for a `master_user=yes` field on the result (per-user master flag). Either mechanism grants the impersonation.

func WithPenalty added in v1.39.0

func WithPenalty(store PenaltyStore, toSecs PenaltyToSecsFunc) ServerOption

WithPenalty attaches a cross-process penalty store. When set, handleAuth runs the following dance for every request:

  1. Lookup penalty for the remote IP, sleep toSecs(count) seconds (timing-leak protection equalises fast/slow paths regardless of penalty).
  2. Run the chain.
  3. On fail → Update(ip, count+1). On success → Update(ip, 0).

Nil store disables; nil toSecs disables the sleep but still records counters (useful for telemetry without enforcement).

func WithPolicy added in v1.39.0

func WithPolicy(checker PolicyChecker, mode PolicyMode) ServerOption

WithPolicy attaches a policy-server hook. The mode controls which lifecycle calls fire. Nil checker disables every hook.

func WithTokenStore added in v1.44.0

func WithTokenStore(ts TokenStore) ServerOption

WithTokenStore attaches the session token store. When set, every successful AUTH response carries a "token=<hex>" field that the backend must present via VERIFY before entering authenticated state. When nil, no token is issued and the VERIFY command always returns FAIL (standalone / test deployments that do not need the token handshake).

func WithUserdb added in v1.39.0

func WithUserdb(u Userdb) ServerOption

WithUserdb attaches a userdb backend to the Server. When set, every successful passdb result is enriched with userdb fields (with prefetch detection — see RunAuth). The userdb fields land in the response with the `userdb_` prefix preserved on the wire, so login pods can use them to set up the mail session without a separate master-protocol round-trip.

type Snapshot added in v1.39.0

type Snapshot struct {
	// contains filtered or unexported fields
}

Snapshot captures the bag's current state so it can be restored later via Rollback. Used by Chain to isolate each passdb's mutations: snapshot before the call, rollback on ResultNext so the next driver sees a clean slate. The snapshot is read-only — every mutating method on Fields is safe to call between Snapshot and Rollback / discard.

Snapshot is O(n) in the current bag size (copies the keys, vals and idx maps); chain depth + bag size are both bounded by config so the cost stays trivial.

type TokenStore added in v1.44.0

type TokenStore interface {
	Issue(username, sessionID, service string) (string, error)
	Validate(tok string) (username, sessionID, service string, ok bool)
}

TokenStore is implemented by authtoken.Store. Abstracted as an interface so tests can provide a stub without importing the concrete package.

type UserInfo added in v1.39.0

type UserInfo struct {

	// Username is the resolved canonical username this UserInfo
	// describes. Mandatory.
	Username string

	// OriginalUser is the username the client supplied before any
	// userdb-side translations (case folding, alias rewrite). Empty
	// when no translation occurred.
	OriginalUser string

	// MasterUser is the master user when this lookup arrived via
	// `master=` delegation. Populated only by Phase AUTH-3 master-
	// user flows; the field exists in PR 1 so the schema is stable.
	MasterUser string

	// LoginUser is the post-impersonation effective user. For non-
	// delegated lookups this equals Username; for master-user flows
	// it is the target user. Phase AUTH-3 populates this.
	LoginUser string

	UID               uint32
	GID               uint32
	Home              string
	Chroot            string
	SystemGroupsUser  string   // override username for system group lookup
	Groups            []string // supplementary group names
	ClientCertPresent bool     // TLS client cert auth was used

	// MailLocation overrides the global mail_location (Dovecot
	// `mail` field).
	MailLocation      string
	MailUID           uint32 // distinct from system UID
	MailGID           uint32 // distinct from system GID
	MailboxFormat     string // maildir | sdbox | mdbox
	MailAttributeDict string // dict URL for RFC 5464 METADATA

	// VolatileDir is the VOLATILEDIR modifier extracted from MailLocation
	// (or set directly via the `volatile_dir` userdb extra field). Carries
	// the raw template string (%u/%n/%d/%h not yet expanded) so callers
	// can expand it against the resolved home after userdb completes.
	VolatileDir string

	// IndexDir is the INDEX= modifier extracted from MailLocation (or set
	// directly via the `index_dir` userdb extra field). Carries the raw
	// template string (%u/%n/%d/%h not yet expanded). When set, per-folder
	// index files (yarilo.index*, yarilo-acl) are stored here instead of
	// co-located with the mailbox data under Home.
	IndexDir string

	// ControlDir is the CONTROL= modifier extracted from MailLocation (or
	// set directly via the `control_dir` userdb extra field). Carries the
	// raw template string (%u/%n/%d/%h not yet expanded). When set,
	// per-folder control files (yarilo-uidlist, subscriptions) are stored
	// here instead of co-located with the mailbox data under Home.
	ControlDir string

	// AltDir is the ALT= modifier extracted from MailLocation (or set
	// directly via the `alt_dir` userdb extra field). Carries the raw
	// template string (%u/%n/%d/%h not yet expanded). When set, messages
	// that have been cold-tiered live under AltDir; reads check both
	// primary (Home) and alt tiers.
	AltDir string

	// QuotaRules is the list of per-user quota rules (Dovecot
	// `quota_rule=` can appear multiple times for nested roots).
	QuotaRules    []string
	QuotaOverFlag string // operator-set override marker (string sentinel)

	// AllowNets is the list of IP / CIDR strings the user is
	// allowed to authenticate from. Empty means no IP restriction.
	AllowNets []string

	NoLogin        bool // reject login outright
	NoDelay        bool // bypass auth-penalty backoff (Phase AUTH-4)
	NoAuthenticate bool // auth disabled for this user
	PassExpired    bool // password expired — client must re-set
	NoPassword     bool // passdb without a password (e.g. OAuth)

	Proxy               bool
	ProxyMaybe          bool   // proxy only when remote host differs
	Host                string // proxy backend host
	Port                int    // proxy backend port (0 = default)
	DestUser            string // override target user in proxy
	ProxyMech           string // SASL mech to use against backend
	ProxyTimeout        int    // seconds
	ProxyRedirectReauth bool
	ProxyNoPipelining   bool
	SSL                 string // yes | any | required
	StartTLS            bool

	MailMaxUserIPConnections int // per-(user, IP)
	MailMaxUserConnections   int // per-user across IPs

	Service   string // IMAP | POP3 | LMTP | submission ...
	LocalName string // SNI hostname the client connected to

	// Forward carries `forward_*` fields that are passed through
	// a proxy chain unchanged. Map key is the field name WITHOUT
	// the `forward_` prefix.
	Forward map[string]string

	// Extra is the catch-all bag for fields not modelled as typed
	// struct members. Anything a userdb driver returns that does
	// not map to a typed field above ends up here, keyed by the
	// raw field name. Wire-level prefix semantics (`userdb_*=`,
	// `auth_*=`) land in Phase AUTH-2.
	Extra map[string]string

	// Password is the stored credential (or scheme-encoded hash)
	// from a passdb lookup. The master-protocol wire serialiser
	// in Phase AUTH-1 PR 2 strips this unconditionally so an
	// admin-side USER request can never leak credentials. Present
	// on the struct so passdb→userdb internal pipelines do not
	// need a parallel type.
	Password string

	// CertName is the TLS client certificate Common Name. Set by
	// the EXTERNAL SASL mechanism (Phase AUTH-5). Internal-only.
	CertName string

	// PolicyResponse is the verbatim body returned by the policy
	// HTTP endpoint in Phase AUTH-6. Internal-only.
	PolicyResponse string
}

UserInfo carries the full set of user fields a userdb lookup can return. Modelled after Dovecot 2.4's auth-fields plus the reserved field names every userdb driver writes (auth-fields.c, auth-request.c, userdb-passwd.c). The struct is intentionally comprehensive — adding fields later means breaking the protocol surface that pkg/authclient + yarilo-backend-api speak, so the shape is fixed up-front during Phase AUTH-1 and the prefix / snapshot / prefetch semantics in later phases extend BEHAVIOUR without touching DATA shape.

Zero values are the "field absent" marker; the master-protocol wire serialiser in Phase AUTH-1 PR 2 skips them. Sensitive fields (Password, CertName, PolicyResponse) are populated server-side but the wire layer filters them out unconditionally — they exist on the struct so internal pipelines can carry them without a parallel struct.

func ParseUserInfo added in v1.39.0

func ParseUserInfo(tokens []string) (*UserInfo, error)

ParseUserInfo reconstructs a UserInfo from a list of wire-format `key=value` tokens (typically the tab-split portion of a master-protocol USER / PASS response after the leading `<verb>\t<id>\t<username>\t...`). The Username is set by the caller from the wire's username token; ParseUserInfo only fills the remainder.

func (*UserInfo) VisitFields added in v1.39.0

func (ui *UserInfo) VisitFields(fn func(key, value string))

VisitFields calls fn for every non-zero typed field on UserInfo in canonical order. Internal-only fields (Password, CertName, PolicyResponse) are NEVER passed to fn — callers that serialise to a wire / bag get them stripped by construction.

Lists are emitted as a single comma-joined value; booleans are emitted only when true (value "yes"); numbers are decimal-rendered. Forward fields emit with the `forward_` prefix; Extra entries emit verbatim. Forward and Extra are iterated in lexicographic key order so two callers serialising the same UserInfo produce byte-identical output.

Shared by master.go's marshalUserInfo (no prefix, tab-joined wire form) and protocol.go's writeUserdbFields (`userdb_` prefix, Fields-bag form) so the field surface stays in lockstep.

type Userdb added in v1.39.0

type Userdb interface {
	Lookup(username string) (*UserInfo, error)
}

Userdb is the contract every userdb backend implements. Unlike Passdb.Authenticate, Lookup takes no password — userdb answers "who is this user?" for admin tooling, master-protocol clients, and the prefetch shortcut (Phase AUTH-2) that lets a passdb short-circuit a redundant userdb round-trip.

Semantics on the return value match Passdb / Chain:

  • (ui != nil, nil) — user found, ui populated
  • (nil, nil) — user unknown in this backend; UserdbChain tries the next entry
  • (nil, err != nil) — backend failure; UserdbChain stops and surfaces the error to the caller

type UserdbChain added in v1.39.0

type UserdbChain []Userdb

UserdbChain composes multiple Userdb backends with first-hit-wins semantics, mirroring the Passdb Chain. The first non-nil response from any backend in the chain is returned; an error from any backend short-circuits the chain (matches Dovecot's USERDB_RESULT_INTERNAL_FAILURE — "stop, do not try fallbacks").

func (UserdbChain) Lookup added in v1.39.0

func (c UserdbChain) Lookup(username string) (*UserInfo, error)

type UserdbIterator added in v1.39.0

type UserdbIterator interface {
	Iterate() ([]string, error)
}

UserdbIterator is implemented by userdb backends that support listing every known user. Used by `yarilo-admin backend user iterate` and by the master-protocol `LIST` command. Optional — a backend that cannot enumerate (e.g. an LDAP search with no suitable filter) simply does not implement it.

type Validator added in v1.39.0

type Validator func(value string) (string, error)

Validator parses a raw field value and returns its canonical string form. Returns an error when the value does not satisfy the field's type contract.

Canonicalisation: validators normalise equivalent inputs to a single representation. `Set("nologin", "TRUE")` then `Set("nologin", "yes")` then `Set("nologin", "1")` all land in the bag as `nologin=yes` — so two callers writing the "same" field produce byte-identical wire output.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL