Documentation
¶
Overview ¶
Package authclient is the Go client for the yarilo-auth master protocol. It opens a single mTLS TCP connection to the master listener (see internal/auth/protocol/master.go), drains the handshake, and provides typed methods for the wire commands:
- Client.Userdb password-less user lookup (USER)
- Client.IterateUsers enumerate every known user (LIST)
- Client.PassdbLookup returns ErrNotImplemented until Passdb.LookupCredentials ships
One Client owns one TCP conn. Commands serialise behind a mutex, so multiple goroutines may call methods concurrently; wire-side ordering is enforced internally. For higher throughput, open additional Clients; there is no pool.
Index ¶
- Variables
- type Client
- func (c *Client) CacheFlush(ctx context.Context, masks []string) (uint32, error)
- func (c *Client) Close() error
- func (c *Client) IssueSession(ctx context.Context, username, sid, ip string) (string, error)
- func (c *Client) IterateUsers(ctx context.Context) ([]string, error)
- func (c *Client) PassdbLookup(ctx context.Context, username string) (*protocol.UserInfo, error)
- func (c *Client) Ping(ctx context.Context) error
- func (c *Client) Userdb(ctx context.Context, username string) (*protocol.UserInfo, error)
- func (c *Client) UserdbFor(ctx context.Context, username, service string) (*protocol.UserInfo, error)
- type Pool
- func (p *Pool) Close() error
- func (p *Pool) IssueSession(ctx context.Context, username, sid, ip string) (string, error)
- func (p *Pool) Userdb(ctx context.Context, username string) (*protocol.UserInfo, error)
- func (p *Pool) UserdbFor(ctx context.Context, username, service string) (*protocol.UserInfo, error)
Constants ¶
This section is empty.
Variables ¶
var ErrClosed = errors.New("authclient: client closed")
ErrClosed signals that a Client was Closed or its connection terminated. Subsequent calls return it so callers can re-Dial rather than hang.
var ErrNotImplemented = errors.New("authclient: PASS not implemented (Phase AUTH-2)")
ErrNotImplemented is returned by Client.PassdbLookup while the master PASS handler still replies `FAIL reason=PASS not implemented`. Callers can errors.Is against it to gate fall-through behaviour.
ErrUnavailable marks a failure to REACH yarilo-auth, as opposed to an answer from it. The distinction is the whole point: a protocol above can say "try again in a moment" for a service being redeployed, where a client told "this server is broken" stops retrying a request that would work seconds later.
The same seam as locks.ErrUnavailable (#1339), for the other dependency: the jmap and fts user resolvers reach auth on the request path, and without a marker their transport failures arrived at the classifiers as ordinary errors and became serverFail / a bare NO (#1402).
Carried only by transport failures. An answer from the service -- NOTFOUND, FAIL, a malformed or unexpected response -- is not unavailability, however unwelcome it is.
Functions ¶
This section is empty.
Types ¶
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client speaks the yarilo-auth master protocol over a single TCP (optionally mTLS-wrapped) connection. Methods are safe for concurrent use: a mutex enforces one in-flight request at a time on the underlying conn.
When the connection breaks (e.g. yarilo-auth restart) the next call redials and retries once. Client.Close disables reconnect permanently.
Construct with Dial / DialContext. Always Client.Close on shutdown so the FIN reaches the server promptly.
func Dial ¶
Dial opens a connection to the yarilo-auth master listener at addr and consumes the handshake. A non-nil tlsCfg wraps the connection in (m)TLS. Only major version 1 is accepted; other versions return a typed error.
func DialContext ¶
DialContext is Dial with a context that bounds the TCP + TLS handshake. After the handshake the context no longer affects the returned Client.
func DialWaiting ¶
func DialWaiting(ctx context.Context, addr string, tlsCfg *tls.Config, wait time.Duration) (*Client, error)
DialWaiting is Dial with a bounded wait for the service to come up.
The two contracts differ and this is the startup one. A process starting while auth is rolling has nobody to tell: exiting turns a few seconds of dependency downtime into a restart loop (#1369). On a REQUEST the opposite holds -- a client is waiting for an answer, and a fast refusal beats a hang -- which is why the per-request dials deliberately do not use this.
A wait of zero or less is a plain Dial, so the waiting can be turned off without a second code path.
func (*Client) CacheFlush ¶
CacheFlush invokes the CACHE-FLUSH verb. An empty masks slice flushes everything; otherwise each mask is sent as a tab-separated argument and entries whose username matches any mask are evicted (glob: `*`, `?`). Returns the evicted-entry count reported by the server.
func (*Client) Close ¶
Close releases the underlying connection. Idempotent. After Close every other method returns ErrClosed.
func (*Client) IssueSession ¶
IssueSession sends a SESSION command and returns a one-time token bound to username. The token is scoped to the "lmtp" service and must be consumed by a VERIFY on the client listener within its TTL.
sid is the warden session ID for this delivery; ip is the originating MTA address. Both are audit-logged only and do not affect token validity.
func (*Client) IterateUsers ¶
IterateUsers runs a LIST and collects streamed usernames until the DONE marker. Backends without enumeration reply FAIL, surfaced as an error.
func (*Client) PassdbLookup ¶
PassdbLookup runs a PASS lookup. The master PASS handler still replies `FAIL reason=PASS not implemented`, so this method always returns ErrNotImplemented; the typed call surface is stable for when it lands.
func (*Client) Ping ¶
Ping asks the master for a sign of life on this connection.
ANY single-line reply counts, including a FAIL: a server too old to know NOOP answers "unknown command", and a server that answers at all is exactly what the probe is asking about. That makes the probe safe against a mixed rollout with no ordering requirement -- the reply's content is not the signal, its arrival is.
func (*Client) Userdb ¶
Userdb runs a USER lookup for username. Returns:
- (ui, nil) - backend hit; ui populated from the wire fields
- (nil, nil) - backend miss (master responded NOTFOUND)
- (nil, err) - wire / parse / backend error
Mirrors the protocol.Userdb "not found is nil/nil" contract.
type Pool ¶
type Pool struct {
// contains filtered or unexported fields
}
Pool keeps a small number of master-protocol connections open for userdb lookups, instead of dialling one per lookup.
The dial is about seven times the work it carries: 3.1ms of TCP, TLS and the master's greeting against a 0.44ms USER lookup, measured against the real service (#1402). It is also a connection per lookup arriving at yarilo-auth -- ~10/s from a modest sandbox load, each with a full handshake on the server side too, which is a cost that lands on the dependency rather than on us.
Modelled on pkg/locks: fixed slots handed out through a channel, so a slot is owned exclusively by whoever holds it and lookups on one connection stay serialised, which the master protocol requires.
func NewPool ¶
NewPool returns a pool of size connections to addr. A size of zero or less is not an error and not a pool: callers get a fresh connection per lookup, which is the behaviour this replaces and the rollback for it.
Nothing is dialled here. A pod that starts while auth is rolling must not fail because of it (#1369); the first lookup pays for the first connection.
func (*Pool) IssueSession ¶
IssueSession issues a session token, reusing a pooled connection when one is available. Same contract as Userdb: the call travels to a connection rather than a connection being opened for the call.