CrIRCd - IRCD (daemon) in Crystal

A Crystal IRC server implementation that follows IRC protocol specifications. While not yet 100% spec-compliant, it provides a solid foundation for IRC server functionality.

Features

Core IRC Functionality

Network & Security

Planned Features

Not Implemented

Quick Start

Requirements

Installation

  1. Clone this repository
  2. Build the server: crystal build --release ./src/circed.cr
  3. Run the server: ./circed

Basic Configuration

The configuration is in config.yml. Basic settings include:

host: "0.0.0.0"
port: 6667
network: "MyIRCNetwork"
max_users: 100
link_password: "server_link_password"
line_database: "data/lines.yml"
allow_die: false
allow_restart: false

IRC Operators

IRC operators are configured with O-line style entries under operators. Clients authenticate with OPER <name> <password>. On success, CrIRCd sends 381 RPL_YOUREOPER and sets user mode +o for global operators or +O for local operators. Users cannot grant themselves +o or +O with MODE; those modes must come from OPER, though users may remove their own operator mode. Operators can use KILL <nickname> :<reason> to disconnect users, REHASH to reload config, CONNECT to start a configured server link, and SQUIT to drop a server link. DIE and RESTART are disabled by default and require allow_die: true or allow_restart: true.

CrIRCd supports IRCd-style line bans for operators. These are common IRCd extensions, not RFC core commands. KLINE <user@host> [duration] :<reason> adds a local user@host ban, GLINE <user@host> [duration] :<reason> adds a network-wide user@host ban across linked CrIRCd servers, and ZLINE <ip-or-cidr> [duration] :<reason> adds a local IP ban. Calling the same command with only a mask removes the line, for example GLINE *@bad.example. Durations can be seconds or compound values like 1w2d3h; omit the duration or use 0 for a permanent line. Active lines are stored in line_database.

Users can set MODE <nick> +w to receive wallops notices from linked servers. CrIRCd treats WALLOPS as a server-originated command and does not expose it as a client operator command.

operators:
  - name: "admin"
    password: "change-this-password"
    hosts:
      - "*[email protected]"
      - "trusted.example.com"

  - name: "local-admin"
    password: "change-this-too"
    local: true
    hosts:
      - "localhost"
      - "*[email protected]"

hosts is optional and defaults to ["*"]. Host masks are matched case-insensitively with * and ? wildcards against the user's hostmask, resolved hostname, and socket host string. Prefer restrictive host masks in production.

Testing

Use the focused test helper during development:

# Fast unit-level specs
scripts/test fast

# Real server integration specs
scripts/test integration

# Both suites
scripts/test all

The integration specs bind fixed local ports and should be run sequentially.

Benchmarks and Capacity

CrIRCd uses max_users as the configured local-client limit. Real concurrency is also bounded by the operating system file-descriptor limit, memory, TLS overhead, channel fanout size, and server-to-server links. Set max_users below the process file-descriptor limit with room for listening sockets, linked servers, logs, and outbound files.

Reference benchmark on an Apple M1 Pro with 16 GB RAM, Crystal 1.17.1, release build, LOG_LEVEL=ERROR, and ulimit -n 8192. The local load generator runs concurrent client fibers against the server and sends QUIT before closing each test socket:

The largest local socket benchmark run completed with 5,000 concurrent connected clients spread across 250 channels. This is the current benchmark config limit, not a hard architectural limit. Larger deployments should raise max_users, raise file-descriptor limits, and benchmark on the target host. As a rule of thumb, a single process needs at least one file descriptor per local client plus headroom for listeners, server links, logs, and outbound files.

The in-memory channel index benchmark uses 20,000 users, 5,000 channels, and 100,000 user-channel memberships:

That supports IRC networks with tens of thousands of users and thousands of channels for membership-heavy operations on one process, assuming channel sizes and message rates are reasonable. Server-to-server networks should be kept to tens of directly linked servers until route-table caching and burst benchmarks are added; message propagation still fans out to linked servers. For large public networks, split users across linked servers and keep very large channels rare, because every channel message is still delivered to each recipient.

CrIRCd uses bounded Crystal channels for per-client and server-link outbound queues, with writer fibers batching socket writes. Channel fanout remains a direct membership iteration instead of one pipe per IRC channel: Crystal channels are best used to communicate between fibers, while channel message delivery still has to visit each recipient. Extra per-channel pipes would add scheduling and backpressure overhead without reducing the O(channel members) delivery cost.

To target 7,000-10,000 registrations per second, run release builds with low log volume, keep DNS asynchronous with a small registration wait, raise file descriptor limits, keep slow clients from blocking fanout with bounded outbound queues, and benchmark with realistic channel sizes and TLS settings.

Run the benchmarks with:

crystal run --release benchmarks/channel_repository_benchmark.cr

crystal build --release -o bin/circed src/circed.cr
ulimit -n 8192
LOG_LEVEL=ERROR bin/circed benchmarks/benchmark_config.yml

Then, from another shell with the same descriptor limit:

ulimit -n 8192
crystal run --release benchmarks/local_client_load.cr -- 127.0.0.1 16680 3500 175 2 128

Supported IRC Surface

CrIRCd currently supports these client-facing commands:

Server-to-server links support handshake, burst, channel membership, user state, message routing, global line-ban synchronization, server query propagation, and netsplit cleanup with state recovery after reconnection.

Unsupported areas include persistent IRC services.

Configuration Reload

The server watches the config file and reloads scalar configuration into memory, but runtime sockets are not reconciled after reload. Restart the process after changing bind host, ports, SSL certificates, linked servers, or limits that need to affect already-running listeners and links.

DNS Hostnames

Client hostnames are resolved asynchronously. New clients start with their IP address immediately, then CrIRCd queues a reverse DNS lookup. Before sending the registration welcome, the client waits up to dns.registration_wait_ms for a verified hostname. If DNS is slow, unavailable, or the PTR result does not forward-confirm back to the client IP, the IP address remains the hostname.

dns:
  enabled: true
  server: "8.8.8.8"
  port: 53
  workers: 4
  queue_size: 1024
  timeout_seconds: 1
  registration_wait_ms: 100
  cache_ttl_seconds: 3600
  negative_cache_ttl_seconds: 300

Keep registration_wait_ms small on high-throughput servers. Increase workers only if DNS latency is high and the queue backs up.

SSL/TLS Configuration

CrIRCd supports secure connections using SSL/TLS for both clients and server-to-server communication.

Quick SSL Setup

  1. Generate test certificates:

    ./generate_ssl_certs.sh
  2. Enable SSL in config.yml:

    ssl:
      enabled: true
      port: 6697
      cert_file: "ssl/server.crt"
      key_file: "ssl/server.key"
      starttls: true
  3. Start the server and connect:

    ./circed config.yml
    
    # Connect with SSL client
    openssl s_client -connect localhost:6697

Advanced SSL Configuration

For production deployments with certificate verification:

ssl:
  enabled: true
  port: 6697
  cert_file: "/path/to/server.crt"
  key_file: "/path/to/server.key"
  ca_file: "/path/to/ca.crt"        # For client cert verification
  verify_mode: true                 # Verify client certificates
  starttls: true                    # Allow STARTTLS upgrade
  require_ssl_for_servers: false    # Require SSL for server links

Server-to-Server SSL

Configure encrypted server links:

linked_servers:
  - host: "irc2.example.com"
    server_name: "irc2.example.com"
    port: 6697
    link_password: "secure_password"
    use_ssl: true
    verify_ssl: true  # Verify server certificate

Client Connection Methods

Direct SSL Connection (Port 6697):

# IRC clients
irssi -c irc.example.com -p 6697 --ssl

# Testing with OpenSSL
openssl s_client -connect localhost:6697

STARTTLS Upgrade (Port 6667):

STARTTLS
# Server responds: 670 :STARTTLS successful, proceed with TLS handshake
# Client performs TLS handshake

SSL Security Features

Production SSL Considerations

  1. Use Valid Certificates: Obtain from trusted CA (Let's Encrypt recommended)
  2. Secure Key Storage: Set appropriate file permissions (600 for private keys)
  3. Certificate Renewal: Implement automatic renewal processes
  4. Monitoring: Log SSL handshake success/failures for security monitoring

Known issues

Contributions

Everyone is welcome to contribute. Fork this repo, make your changes and make a Pull Request explaining what you did and why. And I and others will review it and merge it if it make sense. :)

Maintainers: