Freeq implements a custom SASL mechanism for authenticating IRC users with
AT Protocol (Bluesky) identities. The mechanism name is ATPROTO-CHALLENGE.
Client Server
| |
| CAP REQ :sasl |
|------------------------------>|
| CAP ACK :sasl |
|<------------------------------|
| AUTHENTICATE ATPROTO-CHALLENGE
|------------------------------>|
| AUTHENTICATE <base64 challenge>
|<------------------------------|
| AUTHENTICATE <base64 response>
|------------------------------>|
| 900 RPL_LOGGEDIN |
| 903 RPL_SASLSUCCESS |
|<------------------------------|
The server sends a JSON challenge encoded as base64url:
{
"session_id": "<unique per TCP connection>",
"nonce": "<32 bytes, cryptographically random, base64url>",
"timestamp": <unix epoch seconds>
}
The client responds with base64url-encoded JSON:
{
"did": "did:plc:abc123...",
"method": "crypto" | "pds-session" | "pds-oauth",
"signature": "<base64url signature over raw challenge bytes>",
"pds_url": "https://bsky.social"
}
crypto — Client signs the raw challenge bytes with a key listed in
the DID document's authentication or assertionMethod sections.
Supported curves: secp256k1 (required), ed25519 (recommended).
pds-session — Client provides a Bearer JWT (from an app password
session). Server calls com.atproto.server.getSession on the claimed
PDS to verify the token belongs to the claimed DID.
pds-oauth — Client provides a DPoP-bound OAuth access token.
Server constructs a DPoP proof and calls getSession on the PDS.
--challenge-timeout-secs).did:plc via plc.directorydid:web via HTTPS, then extracts verification keys.When a user authenticates, their nick is bound to their DID. This binding:
- Persists across server restarts (stored in SQLite)
- Prevents other users from using the nick
- Unauthenticated users claiming a registered nick are renamed to GuestXXXX
- Propagated across federated servers via CRDT
MODE +b did:plc:xyz bans by identity rather than hostmask.Freeq adds custom WHOIS numerics:
- 330 (RPL_WHOISACCOUNT): Shows the authenticated DID
- 671: Shows the resolved AT Protocol handle (e.g. chadfowler.com)
- 672: Shows the iroh P2P endpoint ID (if connected via iroh)
All transports feed into the same IRC protocol handler. The server is
transport-agnostic — clients can mix transports freely.
| Transport | Port | Notes |
|---|---|---|
| TCP | 6667 | Standard IRC |
| TLS | 6697 | Standard IRC over TLS |
| WebSocket | configurable | IRC-over-WebSocket at /irc |
| iroh QUIC | auto | NAT-traversing, end-to-end encrypted |
The server advertises its iroh endpoint ID in CAP LS:
CAP * LS :sasl message-tags ... iroh=<endpoint-id>
Clients that support iroh can discover the endpoint and upgrade their
connection to QUIC, gaining NAT traversal and relay fallback.
Servers connect to each other over iroh QUIC links using a JSON-based
protocol. State convergence uses Automerge CRDTs for:
- Channel membership
- Topics
- Nick ownership
- DID-based ops
- Bans
See docs/s2s-audit.md for details on the S2S protocol.
Freeq supports these IRCv3 capabilities:
| Capability | Notes |
|---|---|
sasl |
ATPROTO-CHALLENGE mechanism |
message-tags |
Full tag routing per client |
server-time |
Timestamps on history replay |
batch |
History wrapped in chathistory batch |
multi-prefix |
All prefix chars in NAMES |
echo-message |
Echo own messages back |
account-notify |
ACCOUNT broadcast on auth |
extended-join |
JOIN includes account + realname |
draft/chathistory |
On-demand CHATHISTORY command |
Freeq supports server plugins that hook into events:
| Hook | Description |
|---|---|
on_connect |
New client connection (before registration) |
on_auth |
SASL authentication complete (can override displayed identity) |
on_join |
User joins a channel |
on_message |
PRIVMSG/NOTICE (can suppress or rewrite) |
on_nick_change |
Nick change |
Plugins are compiled into the binary and activated by name via CLI or
TOML config files. See examples/plugins/ for examples.