feat: SASL authentication and IRCv3 capability negotiation - #138
Merged
Conversation
The handshake was a bare NICK/USER pair, so a bot could not identify itself to services. Modern networks need this: +r channels refuse an unauthenticated bot, and the account cloak never applies. Registration now sends CAP LS 302 before NICK/USER, which makes the server hold registration open, and closes with CAP END. SASL PLAIN and SASL EXTERNAL run inside that window. A PASS server password is sent too, before NICK. Credentials go on Server, not behind a State builder. The generated new() connects immediately, so by the time a State exists to configure, the handshake is over. A refused SASL exchange fails the connection. If the network offers no sasl capability, refuses the mechanism, or rejects the credentials, connect returns an error. The bot does not continue unauthenticated. Lines that arrive during the exchange but do not belong to it are handed back to the read loop in arrival order. ERR_NICKNAMEINUSE is the one that matters: the server sends it as soon as it reads NICK, which is in the middle of the exchange, and dropping it would break the fallback nick. base64 is implemented here rather than added as a dependency. SASL needs a few dozen bytes encoded once per connection. PASS and AUTHENTICATE log a redacted form, and Debug redacts every credential, so a password cannot reach the protocol trace.
A reloaded process rebuilt its Server from the inherited environment, which holds only the address. The inherited socket is already registered, so the bot kept working. A later reconnect did not: it re-ran the handshake without credentials and arrived unauthenticated. The generated new() now keeps the Server the caller passed, which carries both the transport and the credentials. Nothing secret goes through the exec environment.
Each test drives State::connect against a scripted server on loopback and asserts the bytes on the wire: the RFC 4616 payload for SASL PLAIN, the empty response for EXTERNAL, multi-line CAP LS reassembly, a PING during the exchange, a nick collision reaching the read loop, and the message of every failure mode. The tokio test-util feature runs the registration timeout on a paused clock, so that test costs no real time.
Add an Authentication section to the README, and show both SASL mechanisms in the TLS example.
chrj
marked this pull request as ready for review
August 17, 2026 10:10
Merged
chrj
pushed a commit
that referenced
this pull request
Aug 17, 2026
## 🤖 New release * `ircbot-macros`: 0.4.2 -> 0.4.3 * `ircbot`: 0.4.2 -> 0.4.3 (✓ API compatible changes) <details><summary><i><b>Changelog</b></i></summary><p> ## `ircbot-macros` <blockquote> ## [0.4.3](ircbot-macros-v0.4.2...ircbot-macros-v0.4.3) - 2026-08-17 ### Added - SASL authentication and IRCv3 capability negotiation ([#138](#138)) ### Other - warn on missing documentation ([#131](#131)) </blockquote> ## `ircbot` <blockquote> ## [0.4.3](v0.4.2...v0.4.3) - 2026-08-17 ### Added - SASL authentication and IRCv3 capability negotiation ([#138](#138)) ### Other - *(deps)* bump testcontainers from 0.27.3 to 0.28.0 ([#135](#135)) - warn on missing documentation ([#131](#131)) </blockquote> </p></details> --- This PR was generated with [release-plz](https://github.com/release-plz/release-plz/). Co-authored-by: chrj-release-app[bot] <272076618+chrj-release-app[bot]@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this changes
The registration handshake was a bare
NICK/USERpair, so a bot could not identify itself to services. Most networks need this:+rchannels refuse an unauthenticated bot, and the account cloak never applies.Registration now sends
CAP LS 302beforeNICK/USER, which makes the server hold registration open, and closes the exchange withCAP END. SASL runs inside that window.The builders are on
Serverand onTlsServer.Why the credentials live on
ServerThe generated
new()connects immediately. By the time aStateexists for awith_*builder to configure, the handshake is over.Serveris the only value the caller supplies early enough, and it already travels intoBlueprint, so a reconnect re-authenticates for free.A refused SASL exchange fails the connection
If the network offers no
saslcapability, refuses the mechanism, or rejects the credentials,connectreturns an error. The bot does not continue unauthenticated. Arriving without your identity is the fault SASL prevents, so it is loud rather than silent.A bot with no credentials sends the same handshake as before, and no
CAPline at all.Two failures this had to avoid
A dropped nick collision. The server answers
NICKas soon as it reads it, which is in the middle of the capability exchange. The handshake buffers every line that does not belong to it and hands them back; the read loop drains those before it touches the socket. Without this the fallback nick would have stopped working.A downgrade after a hot reload. A reloaded process rebuilt its
Serverfrom the inherited environment, which holds only the address. The inherited socket is already registered, so the bot kept working, but a later reconnect re-ran the handshake with no credentials. The generatednew()now keeps theServerthe caller passed. Nothing secret goes through the exec environment.Notes
auth.rs, tested against the RFC 4648 vectors. Thetest-utilfeature is added to the existing tokio dev-dependency so the timeout test runs on a paused clock.PASSandAUTHENTICATElog a redacted form, andDebugredacts every credential, so a password cannot reach the protocol trace. Tests pin this.Testing
18 tests in
ircbot/tests/sasl.rsdrive the realState::connectagainst a scripted server on loopback and assert the bytes on the wire: the RFC 4616 payload for PLAIN, the empty response for EXTERNAL, multi-lineCAP LSreassembly, aPINGduring the exchange, a nick collision reaching the read loop, and the message of every failure mode. 9 unit tests cover base64, redaction, capability selection, andCAPline parsing.cargo test --workspace,cargo clippy --workspace --all-targets --features tls -- -D warnings,cargo fmt --all --check, andcargo docwith-D warningsall pass.