10.1 KiB · text History cac0cb2

OhMyExOhIrcd

A clusterable IRC daemon written in Elixir on top of Phoenix, Horde, and DeltaCrdt — with persistent channel registration, ChanServ-style services, IRC-over-TLS, and ACME cert provisioning baked in.

Status: working dev environment with 196 tests passing. Not yet battle-hardened at scale. PRs welcome.

What’s in the box

Distribution

  • libcluster for node discovery (gossip locally, Kubernetes strategy in k8s).
  • Horde for cluster-wide Channel + user nick singletons and service_set_access-grade services (ChanServ).
  • DeltaCrdt-backed per-channel message log keyed by HLC timestamps, so partitioned nodes can accept writes independently and replay deterministically on heal.
  • Phoenix.PubSub for local fan-out on top of the CRDT.

IRC features

  • RFC 2812-ish protocol: NICK / USER / PASS / CAP / AUTHENTICATE (SASL PLAIN) / JOIN / PART / QUIT / PRIVMSG / NOTICE / PING / PONG / TOPIC / NAMES / WHO / WHOIS / LIST / LUSERS / MOTD / MODE / KICK / INVITE / AWAY / USERHOST / ISON.
  • Channel access prefixes ~ & @ % + (founder / admin / op / halfop / voice) with proper rank-based permission checks.
  • Channel modes: +b/+e/+I (ban / except / invite-exception masks with glob matching), +i (invite-only), +m (moderated), +n (no external), +k <key>, +l <limit>. Mode views via MODE #ch b etc. ban-list-style numerics (367/348/346).
  • ChanServ service with REGISTER / DROP / INFO / OP / DEOP / HOP / DEHOP / VOICE / DEVOICE / TRANSFER / HELP. Service nicks are configurable and reserved at the registry level.
  • Server-initiated PING keepalive with configurable interval + timeout (defaults: 120s / 60s).
  • Hybrid netsplit semantics: classic QUIT for presence on split, CRDT replay of PRIVMSG history on heal.

Auth & persistence

  • Phoenix phx.gen.auth user accounts. IRC clients authenticate via SASL PLAIN or classic PASS email:password against the same Phoenix credentials.
  • Optional allow_anonymous flag for friction-free dev / open networks.
  • Persisted channel registrations (channels table) with founder auto-promotion on join.
  • Persisted channel-level modes (key, limit, flags, mask lists) serialized as JSONB on the channels.modes column — restored on Channel singleton restart.
  • Persisted per-user member access (channel_memberships table) — granting +o to an authenticated user survives restarts and reconnects.

Operations

  • Live LiveView landing page showing version, channel count, user count, cluster size, and per-mode connection instructions.
  • TLS via site_encrypt in :standalone builds (in-process ACME against Let’s Encrypt or the bundled internal CA), OR via externally-mounted cert files (cert-manager, certbot, your reverse proxy) in :kubernetes builds. See :tls_mode below.
  • GitLab CI builds both image variants in parallel.
  • Example deployment manifests for both shapes under deploy/.

Quick start (dev)

# Phoenix
mix setup           # deps + ecto.create + ecto.migrate + assets
mix phx.server      # web UI on http://localhost:4666, IRC on 6667

# Connect with any IRC client:
#   /server localhost 6667
# In dev allow_anonymous is on, so any nickname works.

Visit http://localhost:4666/users/register to make a Phoenix account if you want to test SASL auth or register channels.

TLS / ACME modes

:tls_mode is a compile-time flag in config/config.exs (or set via the TLS_MODE env var when building). Two values:

:standalone : The Phoenix endpoint is wrapped by SiteEncrypt.Phoenix.Endpoint. site_encrypt handles HTTP-01 challenges and obtains / renews certs automatically. In dev it uses an in-process internal CA; in prod, configure ACME_DIRECTORY_URL to Let’s Encrypt staging or production. Cert files end up under ACME_DB_FOLDER and are read by the IRC TLS listener.

:kubernetes : The endpoint is plain Phoenix.Endpoint; site_encrypt is compiled out entirely. The IRC TLS listener reads cert files from explicit IRC_TLS_CERT_PATH + IRC_TLS_KEY_PATH — in a typical k8s deployment those point at a Secret mounted from cert-manager. Renewals propagate automatically because :ssl re-reads keyfile/certfile on each handshake.

Both share the same OhMyExOhIrcd.IRC.TLSListener module; the only diff is who provisions the cert.

Deployment

deploy/
├── kubernetes/        # :kubernetes image variant
│   ├── 00-namespace.yaml
│   ├── 10-rbac.yaml             # ServiceAccount + Role for libcluster
│   ├── 20-postgres.yaml         # baseline StatefulSet
│   ├── 30-secrets.example.yaml  # DATABASE_URL, SECRET_KEY_BASE, …
│   ├── 40-certificate.yaml      # cert-manager Certificate
│   ├── 50-services.yaml         # headless / web / IRC LoadBalancer
│   ├── 60-deployment.yaml       # 3 replicas, mounts cert Secret
│   └── 70-ingress.yaml
└── standalone/        # :standalone image variant
    ├── docker-compose.yml + .env.example
    └── quadlet/                 # podman + systemd units
        ├── ircd.network
        ├── ircd-*.volume
        ├── ircd*.container
        ├── ircd.env.example
        └── README.md

See deploy/README.md for the apply order and operating notes.

Architecture sketch

                          ┌─────────────────┐
                          │  Horde Registry │  (cluster-wide;
                          │                 │   {:channel,name} +
                          │                 │   {:user,nick} keys)
                          └────────┬────────┘
                                   │
       ┌───────────────┐    ┌──────┴──────┐    ┌──────────────┐
       │ Channel pid   │    │ User pid    │    │ ChanServ pid │
       │ (singleton)   │    │ (per conn)  │    │ (singleton)  │
       └───────┬───────┘    └──────┬──────┘    └──────────────┘
               │                   │
               ▼                   │
       ┌───────────────┐           │
       │ MessageLog    │◀──────────┘  (PubSub fan-out
       │ DeltaCrdt     │               on channel topics)
       │ (per node)    │
       └───────────────┘
  • Channel owns membership, topic, modes, mask lists, in-memory state. Mutations to non-prefix modes persist to channels.modes; member access changes persist to channel_memberships.
  • MessageLog is one DeltaCrdt per node holding every channel’s PRIVMSG log keyed by {channel, hlc}. Nodes mesh via set_neighbours (driven by NodeListener). The same drain path fans out live messages and on-heal replay, so receivers don’t care whether something was just typed or just merged.
  • Connection is a ThousandIsland handler per TCP socket. It parses RFC-2812 frames, maintains per-conn state, subscribes to PubSub channel topics, and registers itself in the Horde registry under {:user, downcased_nick}. PINGs are server-initiated on inactivity.

Build variants

.gitlab-ci.yml runs two Kaniko jobs on push to master, publishing:

  • $REGISTRY/oh_my_ex_oh_ircd:standalone-<sha> (+ :standalone-master, :master, :latest)
  • $REGISTRY/oh_my_ex_oh_ircd:kubernetes-<sha> (+ :kubernetes-master)

Both come from the same Dockerfile; the only difference is the TLS_MODE build-arg, which the runtime config picks up via Application.compile_env.

Tests

mix test

async: true integration tests use start_supervised! to spin up per-test ThousandIsland listeners on ephemeral ports; async: false DB tests share the sandbox in :shared mode. The TLS handshake test uses SiteEncrypt.Phoenix.Test.clean_restart/1 to drive the internal CA. Full suite is ~37s on a laptop.

Project layout

lib/oh_my_ex_oh_ircd/          # main app
├── accounts/                  # phx.gen.auth
├── channels/                  # ChannelRecord + ChannelMembership schemas
├── channels.ex                # registration context
├── cluster/                   # Horde + libcluster glue
├── irc/                       # protocol, channel, connection, services
│   ├── auth.ex                # SASL/PASS verification
│   ├── chan_serv.ex
│   ├── channel.ex             # the singleton GenServer
│   ├── connection.ex          # ThousandIsland handler
│   ├── hlc.ex                 # hybrid logical clock
│   ├── listener.ex            # plain IRC listener (6667)
│   ├── message.ex             # RFC parser/encoder
│   ├── message_log.ex         # per-node DeltaCrdt
│   ├── services.ex            # boot-time ChanServ starter
│   ├── stats.ex
│   └── tls_listener.ex        # TLS IRC listener (6697)
└── themes.ex
lib/oh_my_ex_oh_ircd_web/      # Phoenix UI
└── live/home_live.ex          # the LiveView landing
priv/repo/migrations/          # users + channels + channel_memberships
deploy/                        # k8s + standalone manifests

Learn more about the underpinnings