Skip to content

Latest commit

Β 

History

2,034 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

freeq logo

freeq

IRC server and client with AT Protocol (Bluesky) identity authentication, end-to-end encrypted channels, iroh QUIC transport, peer-to-peer DMs, and federated server-to-server clustering.

Users authenticate with their Bluesky identity via a custom SASL mechanism (ATPROTO-CHALLENGE). Standard IRC clients connect as guests. Authenticated users get their DID bound to their connection β€” visible via WHOIS, enforced for nick ownership, and usable for DID-based bans, invites, and persistent ops.

Try it now: irc.freeq.at

Web Client

The web client at irc.freeq.at provides:

  • AT Protocol OAuth login β€” sign in with your Bluesky identity
  • Channel policy gates β€” channels can require credential verification to join
  • GitHub verification β€” prove repo collaborator or org membership status
  • Bluesky social graph gates β€” prove you follow someone (no OAuth needed)
  • Moderator appointments β€” ops issue signed credentials for halfop (+h)
  • Automatic role escalation β€” credentials auto-grant IRC modes (op, halfop, voice)
  • Shareable invite links β€” https://irc.freeq.at/join/#channel
  • Message editing, deletion, reactions, threads β€” changing a message requires the sender's signature; current clients sign automatically
  • End-to-end encrypted channels

Demo Channels

Channel Policy What it demonstrates
#demo-follow Must follow @chadfowler.com on Bluesky Social graph verification (zero OAuth)
#demo-github Open join, freeq-irc/freeq collaborators get auto-op Layered credentials + role escalation
#demo-moderation Open join, moderators appointed via credentials Credential-based moderation pipeline

Architecture

freeq-server/       IRC server with SASL, WebSocket, iroh, S2S federation
freeq-app/          React web client (Vite + Tailwind)
freeq-auth-broker/  AT Protocol OAuth broker (persistent sessions)
freeq-sdk/          Reusable client SDK (connect, auth, events, E2EE, P2P)
freeq-tui/          Terminal UI client built on the SDK
freeq-site/         Marketing site (freeq.at)

The SDK exposes a (ClientHandle, Receiver<Event>) pattern β€” any UI or bot can consume events and send commands.

Transport Stack

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚            IRC Wire Protocol             β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚   TCP    β”‚   TLS    β”‚WebSocket β”‚  iroh   β”‚
β”‚  :6667   β”‚  :6697   β”‚  :8080   β”‚  QUIC   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

All transports feed into the same handle_generic() handler β€” the IRC protocol is transport-agnostic. Each transport is zero-cost when not enabled.

Quick Start

Build

cargo build --release

Run the Server

# Minimal: plain TCP only, in-memory
cargo run --release --bin freeq-server

# With persistence
cargo run --release --bin freeq-server -- --db-path data/irc.db

# With TLS
cargo run --release --bin freeq-server -- \
  --tls-cert certs/cert.pem --tls-key certs/key.pem

# With WebSocket + REST API
cargo run --release --bin freeq-server -- --web-addr 0.0.0.0:8080

# With iroh transport (QUIC, NAT-traversing)
cargo run --release --bin freeq-server -- --iroh

# Full production setup
cargo run --release --bin freeq-server -- \
  --listen-addr 0.0.0.0:6667 \
  --tls-listen-addr 0.0.0.0:6697 \
  --tls-cert /etc/letsencrypt/live/example.com/fullchain.pem \
  --tls-key /etc/letsencrypt/live/example.com/privkey.pem \
  --db-path ./irc.db \
  --web-addr 0.0.0.0:8080 \
  --iroh

Generate a self-signed cert for local development:

mkdir -p certs
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 \
  -keyout certs/key.pem -out certs/cert.pem -days 365 -nodes \
  -subj "/CN=localhost" \
  -addext "subjectAltName=DNS:localhost,IP:127.0.0.1"

Connect with the TUI Client

# Guest (no auth)
cargo run --release --bin freeq-tui -- 127.0.0.1:6667 mynick

# Bluesky OAuth (opens browser)
cargo run --release --bin freeq-tui -- 127.0.0.1:6697 mynick \
  --handle alice.bsky.social

# App password fallback
cargo run --release --bin freeq-tui -- 127.0.0.1:6667 mynick \
  --handle alice.bsky.social --app-password xxxx-xxxx-xxxx-xxxx

# Auto-join channels
cargo run --release --bin freeq-tui -- 127.0.0.1:6667 mynick \
  -c '#general,#random'

# Explicit iroh transport
cargo run --release --bin freeq-tui -- 127.0.0.1:6667 mynick \
  --iroh-addr <endpoint-id>

# Vi keybindings
cargo run --release --bin freeq-tui -- 127.0.0.1:6667 mynick --vi

Iroh auto-discovery: When connecting to a server that has --iroh enabled, the TUI probes CAP LS for the iroh=<endpoint-id> capability and auto-upgrades to iroh QUIC transport. No manual endpoint ID needed.

OAuth sessions are cached to ~/.config/freeq-tui/<handle>.session.json so you don't need to re-authenticate on every launch.

Connect with a Standard IRC Client

Any IRC client works as a guest β€” irssi, WeeChat, HexChat, LimeChat, etc. Connect to 127.0.0.1:6667 (plain) or 127.0.0.1:6697 (TLS). No special configuration needed.

Connect via WebSocket

When --web-addr is set, the server accepts WebSocket connections at ws://<addr>/irc. A test HTML client is included at freeq-server/test-client.html.

Authentication

SASL ATPROTO-CHALLENGE

The server implements a custom SASL mechanism for AT Protocol identity:

  1. Client requests CAP sasl, then AUTHENTICATE ATPROTO-CHALLENGE
  2. Server sends a challenge: base64url(json { session_id, nonce, timestamp })
  3. Client responds with one of:
    • Crypto signature (method: "crypto"): Signs challenge bytes with a private key listed in the DID document
    • PDS session (method: "pds-session"): Sends an app-password JWT; server verifies against the PDS
    • PDS OAuth (method: "pds-oauth"): Sends a DPoP-bound access token with proof; server verifies against the PDS
  4. Server verifies, emits 903 (success) or 904 (failure)
  5. Client sends CAP END, registration completes

Security Properties

  • Each challenge contains a cryptographically random nonce
  • Challenges are invalidated after use (no replay)
  • Challenge validity window: configurable, default 60 seconds
  • Private keys never leave the client
  • PDS URL is verified against the DID document before accepting session tokens
  • Supported key types: secp256k1 (MUST), ed25519 (SHOULD)

What Authentication Gets You

  • Nick is bound to your DID β€” no one else can use it
  • WHOIS shows your DID and Bluesky handle
  • You can be banned or invited by DID (survives reconnect/nick changes)
  • Persistent channel ops tied to your DID (survive reconnects and work across federated servers)
  • Your identity is cryptographically verifiable

Transports

TCP / TLS (Standard)

Standard IRC on port 6667 (plain) and 6697 (TLS). TLS auto-detected by port in the client. Always available.

WebSocket

Enabled with --web-addr. Accepts WebSocket IRC at /irc. Uses the same IRC wire protocol β€” WebSocket is a transport, not a new protocol. Includes a read-only REST API at /api/v1/ (channels, members, topics, messages).

iroh (QUIC)

Enabled with --iroh. Provides NAT-traversing encrypted QUIC connections via iroh. The server generates a persistent secret key (iroh-key.secret) on first run β€” endpoint ID is stable across restarts.

The server advertises its iroh endpoint ID in CAP LS:

CAP * LS :sasl message-tags iroh=44f1415c9db30989...

Clients auto-discover and upgrade to iroh when available.

End-to-End Encryption (E2EE)

Client-side channel encryption using AES-256-GCM with HKDF-SHA256 key derivation from a shared passphrase. The server relays ciphertext unchanged.

/encrypt <passphrase>    Enable E2EE for current channel
/decrypt                 Disable E2EE for current channel

Wire format: ENC1:<nonce-b64>:<ciphertext-b64> β€” version-tagged, uses the message body for robustness. All channel members must use the same passphrase.

Peer-to-Peer Encrypted DMs

Direct encrypted messaging between clients via iroh QUIC, bypassing the server entirely.

/p2p start               Start your P2P endpoint
/p2p id                  Show your P2P endpoint ID
/p2p connect <id>        Connect to a peer
/p2p msg <id> <message>  Send a direct message

P2P conversations appear in dedicated p2p:<short-id> buffers. Wire format is newline-delimited JSON (not IRC protocol). ALPN: freeq/p2p-dm/1.

P2P endpoint IDs are visible in WHOIS (numeric 672).

Server-to-Server Federation (S2S)

Servers cluster over iroh QUIC connections. Each server maintains its own local state and syncs channel membership, messages, topics, and DID-based ops across the federation.

Setup

# Server A: just enable iroh (accepts incoming S2S connections)
cargo run --release --bin freeq-server -- --iroh

# Server B: enable iroh + connect to Server A
cargo run --release --bin freeq-server -- --iroh \
  --s2s-peers <server-a-endpoint-id>

Server A doesn't need --s2s-peers β€” it accepts incoming S2S connections automatically when --iroh is enabled.

What Syncs

Feature Sync behavior
JOIN/PART/QUIT Membership tracked per origin server
PRIVMSG Channel messages relayed to all peers
TOPIC Topic changes propagate
DID-based ops Persistent ops sync via CRDT
Founder First-write-wins CRDT resolution
NAMES Includes both local and remote members
WHOIS Shows DID, handle, and origin for remote users

CRDT-Based State Convergence

Channel authority (founder, DID-based ops) uses Automerge CRDTs for conflict-free convergence. Presence is NOT in the CRDT β€” it's S2S event-driven to avoid ghost users when servers crash.

  • Founder resolution: Deterministic min-actor-wins β€” concurrent claims converge deterministically, late entrants cannot overwrite after sync
  • DID ops: Union merge β€” grants propagate, revocations propagate
  • Provenance tracking: All CRDT writes carry origin peer + authorizing DID
  • Authority boundaries: Soft enforcement validates who can write each key-space
  • Event dedup: S2S events carry unique IDs; bounded LRU prevents replay
  • Peer identity: CRDT sync keyed by iroh endpoint ID (cryptographic), not server name (untrusted). Hello handshake binds transport to logical identity.
  • Compaction: Periodic snapshot + reload bounds doc growth in long-lived deployments
  • Async-safe: CRDT uses tokio::sync::Mutex β€” no runtime thread blocking
  • No timestamps in authority decisions (spoofable by rogue servers)

S2S Acceptance Tests

# Run against two live servers
LOCAL_SERVER=localhost:6667 REMOTE_SERVER=irc.freeq.at:6667 \
  cargo test -p freeq-server --test s2s_acceptance -- --nocapture --test-threads=1

9 tests verify: connectivity, bidirectional message relay, NAMES sync, topic sync, PART/QUIT cleanup, and late-joiner state.

IRC Features

Standard IRC

Full compatibility with RFC 1459/2812 basics:

  • NICK, USER, JOIN, PART, PRIVMSG, NOTICE, QUIT
  • NAMES (query channel membership on demand)
  • PING/PONG (client and server keepalive)
  • WHOIS (shows DID, handle, iroh ID for authenticated users)
  • CTCP ACTION (/me)
  • Multiple channels, private messages

Channel Modes

Mode Description
+o nick Channel operator
+v nick Voice
+b mask Ban (hostmask *!*@host or DID did:plc:xyz)
+i Invite-only
+t Topic lock (ops only)
+k key Channel key (password)

DID-Aware Features

  • DID bans (MODE #chan +b did:plc:xyz): Bans by identity, not just hostmask. Survives nick changes and reconnects.
  • DID invites (INVITE nick #chan): If the user is authenticated, the invite is stored by DID and survives reconnect.
  • Nick ownership: Once an authenticated user claims a nick, guests and other DIDs cannot use it. If an unauthenticated user tries to take a registered nick during SASL negotiation, they're renamed to GuestXXXX at registration time.
  • Persistent DID-based ops: When an authenticated user is opped, their DID is recorded. They're auto-opped on rejoin β€” even on a different server in the federation. Channel founders (first authenticated user to create a channel) can never be de-opped.

Message History

The server stores the last 100 messages per channel. When you join, recent history is replayed as standard PRIVMSG β€” works with any IRC client, no special protocol extension needed.

Rich Media (IRCv3 Message Tags)

Rich media is supported through IRCv3 message tags, giving multipart/alternative semantics β€” the same content in two representations:

  • Tags: Structured metadata (content-type, URL, dimensions, alt text)
  • Body: Plain text fallback (description + URL)
@content-type=image/jpeg;media-url=https://cdn.bsky.app/img/...;media-alt=Sunset;media-w=1200;media-h=800 :alice!a@host PRIVMSG #photos :Sunset https://cdn.bsky.app/img/...
Client What they see
irssi, WeeChat Sunset https://cdn.bsky.app/img/... (clickable link)
freeq-tui πŸ–Ό [image/jpeg] Sunset 1200Γ—800 https://cdn.bsky.app/img/...

Media is hosted externally (AT Protocol PDS blob storage). The IRC server never handles media bytes β€” it just relays tagged messages.

Supported tag keys:

Tag Description
content-type MIME type (e.g. image/jpeg, video/mp4)
media-url URL where the media can be fetched
media-alt Alt text / description
media-w Width in pixels
media-h Height in pixels
media-blurhash Blurhash placeholder
media-size File size in bytes
media-filename Original filename

Rate Limiting

Token bucket rate limiter (10 commands/second) kicks in after registration. The initial connection burst is not rate-limited, so clients that send many commands on connect (like LimeChat) work correctly.

TUI Client

Status Bar

The status bar shows:

  • Transport badge: Colored indicator (red=TCP, green=TLS, cyan=WS, magenta=Iroh)
  • Nick: Your current nick
  • Auth: Authenticated DID or "guest"
  • Uptime: Connection duration

Keybindings

Emacs mode (default):

Key Action
Ctrl-A / Home Beginning of line
Ctrl-E / End End of line
Ctrl-F / Right Forward char
Ctrl-B / Left Back char
Alt-F Forward word
Alt-B Back word
Ctrl-D Delete char
Ctrl-H / Backspace Delete back
Ctrl-K Kill to end of line
Ctrl-U Kill to beginning
Ctrl-W Kill word back
Alt-D Kill word forward
Ctrl-Y Yank (paste kill ring)
Ctrl-T Transpose chars
Alt-U Uppercase word
Alt-L Lowercase word
Alt-C Capitalize word
Tab Nick completion
Up / Down Input history
Ctrl-N / Alt-N Next buffer
Ctrl-P / Alt-P Previous buffer
BackTab (Shift-Tab) Previous buffer
PageUp / PageDown Scroll messages
Ctrl-C / Ctrl-Q Quit

Vi mode (--vi):

Normal mode: h/l move, w/b/e word motion, 0/$ line edges, i/a/I/A enter insert, x/X/D/C/S/s delete/change, p/P paste, k/j history, dd clear line. Insert mode: standard typing, Esc to exit to normal mode.

Commands

/join #channel          Join a channel
/part [#channel]        Leave current or named channel
/msg nick message       Private message
/me action              CTCP ACTION
/topic [text]           View or set channel topic
/mode +o/-o nick        Op/deop
/mode +v/-v nick        Voice/devoice
/mode +b [mask]         Ban (or list bans)
/mode +i/-i             Invite-only
/mode +t/-t             Topic lock
/mode +k/-k [key]       Channel key
/op nick                Shortcut for /mode +o
/deop nick              Shortcut for /mode -o
/voice nick             Shortcut for /mode +v
/kick nick [reason]     Kick from channel
/ban mask               Ban user
/unban mask             Remove ban
/invite nick            Invite to current channel
/whois nick             Query user info
/names [#channel]       List channel members
/raw <line>             Send raw IRC line
/encrypt <passphrase>   Enable E2EE for current channel
/decrypt                Disable E2EE for current channel
/p2p start              Start P2P endpoint
/p2p id                 Show your P2P endpoint ID
/p2p connect <id>       Connect to a peer
/p2p msg <id> <text>    Send P2P direct message
/net                    Show/hide network info popup
/debug                  Toggle raw IRC line display
/quit [message]         Disconnect
/help                   Show commands

Network Info Popup (/net)

Shows: transport type, server address, connection state, uptime, nick, authenticated DID, iroh endpoint ID, E2EE channels, P2P DM status. Close with Esc or q.

Debug Mode (/debug)

Toggles raw IRC line display in the status buffer (prefixed with ←). Useful for diagnosing protocol issues.

REST API

When --web-addr is set, a read-only REST API is available:

Endpoint Description
GET /api/v1/channels List all channels
GET /api/v1/channels/{name} Channel info (topic, modes, member count)
GET /api/v1/channels/{name}/members Channel member list
GET /api/v1/channels/{name}/topic Channel topic
GET /api/v1/channels/{name}/messages Recent messages (with pagination)
GET /api/v1/stats Server stats

All writes go through IRC β€” the REST API is strictly read-only.

Server Configuration

freeq-server [OPTIONS]

Options:
  --listen-addr <ADDR>            Plain TCP address [default: 127.0.0.1:6667]
  --tls-listen-addr <ADDR>        TLS address [default: 127.0.0.1:6697]
  --tls-cert <PATH>               TLS certificate PEM file
  --tls-key <PATH>                TLS private key PEM file
  --server-name <NAME>            Server name [default: freeq]
  --challenge-timeout-secs <N>    SASL challenge validity [default: 60]
  --config <PATH>                 Read options from a TOML file (flags override)
  --check-config                  Validate configuration and exit
  --db-path <PATH>                SQLite database path (omit for in-memory)
  --migrate-to <VERSION>          Run the schema ladder to VERSION and exit
  --web-addr <ADDR>               HTTP/WebSocket listener address
  --iroh                          Enable iroh QUIC transport
  --iroh-port <PORT>              UDP port for iroh (default: random)
  --s2s-peers <ID,ID,...>         S2S peer iroh endpoint IDs
  --s2s-peer-api <ID=URL,...>     Where each S2S peer serves its users' signing keys

Persistence

When --db-path is set, the server persists:

  • Message history β€” all channel messages, queryable with pagination
  • Channel state β€” topics, modes (+t, +i, +k), channel keys
  • Bans β€” hostmask and DID bans survive restarts
  • DID-nick bindings β€” nick ownership persists across server restarts

Without --db-path, the server runs entirely in-memory. The database uses SQLite with WAL mode for good concurrent read performance. Persistence failures are logged but do not crash the server.

Every flag can also live in a TOML file passed via --config server.toml (or FREEQ_CONFIG): keys are the flag names with underscores (listen_addr = "0.0.0.0:6667"), lists are arrays, and precedence is CLI flag > environment variable > file > default, so existing flag-based deployments work unchanged. A typo'd key is a startup error that names the key. --migrate-to is deliberately CLI-only. A full worked example ships as server.toml.example, and a test keeps it in sync with the real schema.

Schema migrations run automatically (and upward only) at startup. A server binary refuses to open a database stamped with a newer schema than it knows, so before rolling back to an older binary, downgrade the schema first: freeq-server --db-path <PATH> --migrate-to <VERSION> runs the ladder to the named version and exits. Downgrades stop with an error at any migration that is irreversible by design.

Deployment

See deploy/README.md for example VPS setup and deployment instructions.

Tests

# Unit + integration tests
cargo test

# S2S federation acceptance tests (9 tests, requires two live servers)
LOCAL_SERVER=localhost:6667 REMOTE_SERVER=irc.freeq.at:6667 \
  cargo test -p freeq-server --test s2s_acceptance -- --nocapture --test-threads=1

153 tests covering:

  • SDK (44): IRC parsing (with tag support), tag escaping roundtrip, DID document parsing, key generation/signing/verification, multibase/multicodec, challenge response encoding, SASL signer variants, media attachment roundtrip, link preview roundtrip, media type detection
  • Server unit (33 + 12 CRDT): Message parsing (with tags), tag escaping, SASL challenge store (create, take, replay, expiry, forged nonce), channel state, database roundtrips (channels, bans, messages, identities), CRDT tests (founder deterministic min-actor, founder not overwritten after sync, DID ops sync, topic provenance, authority validation, compaction, metrics, ban provenance)
  • Integration (27): Guest connection, secp256k1 auth, ed25519 auth, wrong key rejection, unknown DID rejection, expired challenge rejection, replayed nonce rejection, channel messaging, mixed auth/guest, nick collision, channel topic, topic lock, channel ops/kick, hostmask bans, DID bans, invite-only, message history replay, nick ownership, quit broadcast, channel key (+k), TLS connection, rich media tag passthrough, persistence (messages, topics, bans, nick ownership survive restart)
  • S2S acceptance (9): Connectivity, bidirectional message relay, NAMES sync, topic sync, PART/QUIT cleanup, late-joiner state

Protocol Notes

Deviations from the Spec

  • Challenge uses JSON encoding (not a binary format) for debuggability
  • PDS session verification is an additional auth method beyond the spec's crypto-only approach β€” it enables OAuth login without requiring users to manage raw signing keys
  • History replay uses standard PRIVMSG (no custom extension or batch)

IRCv3 Compatibility

  • CAP negotiation follows IRCv3 CAP LS 302 / CAP REQ / CAP END
  • SASL flow follows IRCv3 SASL specification with a custom mechanism name
  • message-tags capability follows the IRCv3 message tags specification
  • Media tags use vendor-prefixed names (content-type, media-url, etc.)
  • Server advertises iroh=<endpoint-id> in CAP LS for transport discovery
  • ATPROTO-CHALLENGE could be proposed as an IRCv3 WG mechanism

Plugins

Freeq supports a plugin system for custom server behavior. Plugins hook into events like authentication, message delivery, and channel joins.

# Load a plugin via CLI
freeq-server --plugin "identity-override:handle=timesync.bsky.social,display_id=3|337"

# Load plugins from a directory of TOML configs
freeq-server --plugin-dir ./examples/plugins/

See examples/plugins/ for example configurations and docs/PROTOCOL.md for the full plugin hook reference.

Documentation

License

MIT

About

What if we could go back to using irc?

Resources

Code of conduct

Contributing

Security policy

Stars

74 stars

Watchers

3 watching

Forks

Releases

Packages

Used by

Contributors

Languages