neiam
/oh-my-ex-oh-ircd
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
Log in to mark this repository.
README
cac0cb2 · gmorell · 2026-05-19 04:16
Files changed
modified
README.md
+227
−11
@@ -1,18 +1,234 @@
| 1 | 1 | # OhMyExOhIrcd |
| 2 | 2 | |
| 3 | −To start your Phoenix server: | |
| 3 | +A clusterable IRC daemon written in Elixir on top of Phoenix, Horde, and | |
| 4 | +DeltaCrdt — with persistent channel registration, ChanServ-style | |
| 5 | +services, IRC-over-TLS, and ACME cert provisioning baked in. | |
| 4 | 6 | |
| 5 | −* Run `mix setup` to install and setup dependencies | |
| 6 | −* Start Phoenix endpoint with `mix phx.server` or inside IEx with `iex -S mix phx.server` | |
| 7 | +> Status: working dev environment with 196 tests passing. Not yet | |
| 8 | +> battle-hardened at scale. PRs welcome. | |
| 7 | 9 | |
| 8 | −Now you can visit [`localhost:4000`](http://localhost:4000) from your browser. | |
| 10 | +## What's in the box | |
| 9 | 11 | |
| 10 | −Ready to run in production? Please [check our deployment guides](https://hexdocs.pm/phoenix/deployment.html). | |
| 12 | +**Distribution** | |
| 11 | 13 | |
| 12 | −## Learn more | |
| 14 | + * [libcluster](https://github.com/bitwalker/libcluster) for node | |
| 15 | + discovery (gossip locally, Kubernetes strategy in k8s). | |
| 16 | + * [Horde](https://github.com/derekkraan/horde) for cluster-wide | |
| 17 | + Channel + user nick singletons and `service_set_access`-grade | |
| 18 | + services (ChanServ). | |
| 19 | + * [DeltaCrdt](https://github.com/derekkraan/delta_crdt_ex)-backed | |
| 20 | + per-channel message log keyed by HLC timestamps, so partitioned | |
| 21 | + nodes can accept writes independently and replay deterministically | |
| 22 | + on heal. | |
| 23 | + * Phoenix.PubSub for local fan-out on top of the CRDT. | |
| 13 | 24 | |
| 14 | −* Official website: https://www.phoenixframework.org/ | |
| 15 | −* Guides: https://hexdocs.pm/phoenix/overview.html | |
| 16 | −* Docs: https://hexdocs.pm/phoenix | |
| 17 | −* Forum: https://elixirforum.com/c/phoenix-forum | |
| 18 | −* Source: https://github.com/phoenixframework/phoenix | |
| 25 | +**IRC features** | |
| 26 | + | |
| 27 | + * RFC 2812-ish protocol: `NICK / USER / PASS / CAP / AUTHENTICATE | |
| 28 | + (SASL PLAIN) / JOIN / PART / QUIT / PRIVMSG / NOTICE / PING / PONG | |
| 29 | + / TOPIC / NAMES / WHO / WHOIS / LIST / LUSERS / MOTD / MODE / | |
| 30 | + KICK / INVITE / AWAY / USERHOST / ISON`. | |
| 31 | + * Channel access prefixes `~ & @ % +` (founder / admin / op / | |
| 32 | + halfop / voice) with proper rank-based permission checks. | |
| 33 | + * Channel modes: `+b/+e/+I` (ban / except / invite-exception masks | |
| 34 | + with glob matching), `+i` (invite-only), `+m` (moderated), | |
| 35 | + `+n` (no external), `+k <key>`, `+l <limit>`. Mode views via | |
| 36 | + `MODE #ch b` etc. ban-list-style numerics (367/348/346). | |
| 37 | + * ChanServ service with `REGISTER / DROP / INFO / OP / DEOP / HOP | |
| 38 | + / DEHOP / VOICE / DEVOICE / TRANSFER / HELP`. Service nicks are | |
| 39 | + configurable and reserved at the registry level. | |
| 40 | + * Server-initiated PING keepalive with configurable interval + | |
| 41 | + timeout (defaults: 120s / 60s). | |
| 42 | + * Hybrid netsplit semantics: classic `QUIT` for presence on split, | |
| 43 | + CRDT replay of PRIVMSG history on heal. | |
| 44 | + | |
| 45 | +**Auth & persistence** | |
| 46 | + | |
| 47 | + * Phoenix [`phx.gen.auth`](https://hexdocs.pm/phoenix/Mix.Tasks.Phx.Gen.Auth.html) | |
| 48 | + user accounts. IRC clients authenticate via SASL PLAIN or | |
| 49 | + classic `PASS email:password` against the same Phoenix | |
| 50 | + credentials. | |
| 51 | + * Optional `allow_anonymous` flag for friction-free dev / open | |
| 52 | + networks. | |
| 53 | + * Persisted channel registrations (`channels` table) with founder | |
| 54 | + auto-promotion on join. | |
| 55 | + * Persisted channel-level modes (key, limit, flags, mask lists) | |
| 56 | + serialized as JSONB on the `channels.modes` column — restored on | |
| 57 | + Channel singleton restart. | |
| 58 | + * Persisted per-user member access (`channel_memberships` table) — | |
| 59 | + granting `+o` to an authenticated user survives restarts and | |
| 60 | + reconnects. | |
| 61 | + | |
| 62 | +**Operations** | |
| 63 | + | |
| 64 | + * Live LiveView landing page showing version, channel count, user | |
| 65 | + count, cluster size, and per-mode connection instructions. | |
| 66 | + * TLS via [site_encrypt](https://github.com/sasa1977/site_encrypt) in | |
| 67 | + `:standalone` builds (in-process ACME against Let's Encrypt or | |
| 68 | + the bundled internal CA), OR via externally-mounted cert files | |
| 69 | + (cert-manager, certbot, your reverse proxy) in `:kubernetes` | |
| 70 | + builds. See `:tls_mode` below. | |
| 71 | + * GitLab CI builds both image variants in parallel. | |
| 72 | + * Example deployment manifests for both shapes under `deploy/`. | |
| 73 | + | |
| 74 | +## Quick start (dev) | |
| 75 | + | |
| 76 | +```sh | |
| 77 | +# Phoenix | |
| 78 | +mix setup # deps + ecto.create + ecto.migrate + assets | |
| 79 | +mix phx.server # web UI on http://localhost:4666, IRC on 6667 | |
| 80 | + | |
| 81 | +# Connect with any IRC client: | |
| 82 | +# /server localhost 6667 | |
| 83 | +# In dev allow_anonymous is on, so any nickname works. | |
| 84 | +``` | |
| 85 | + | |
| 86 | +Visit <http://localhost:4666/users/register> to make a Phoenix account | |
| 87 | +if you want to test SASL auth or register channels. | |
| 88 | + | |
| 89 | +## TLS / ACME modes | |
| 90 | + | |
| 91 | +`:tls_mode` is a **compile-time** flag in `config/config.exs` (or set | |
| 92 | +via the `TLS_MODE` env var when building). Two values: | |
| 93 | + | |
| 94 | +`:standalone` | |
| 95 | + : The Phoenix endpoint is wrapped by `SiteEncrypt.Phoenix.Endpoint`. | |
| 96 | + site_encrypt handles HTTP-01 challenges and obtains / renews | |
| 97 | + certs automatically. In dev it uses an in-process internal CA; | |
| 98 | + in prod, configure `ACME_DIRECTORY_URL` to Let's Encrypt | |
| 99 | + staging or production. Cert files end up under | |
| 100 | + `ACME_DB_FOLDER` and are read by the IRC TLS listener. | |
| 101 | + | |
| 102 | +`:kubernetes` | |
| 103 | + : The endpoint is plain `Phoenix.Endpoint`; site_encrypt is | |
| 104 | + compiled out entirely. The IRC TLS listener reads cert files | |
| 105 | + from explicit `IRC_TLS_CERT_PATH` + `IRC_TLS_KEY_PATH` — in a | |
| 106 | + typical k8s deployment those point at a Secret mounted from | |
| 107 | + cert-manager. Renewals propagate automatically because `:ssl` | |
| 108 | + re-reads `keyfile`/`certfile` on each handshake. | |
| 109 | + | |
| 110 | +Both share the same `OhMyExOhIrcd.IRC.TLSListener` module; the only | |
| 111 | +diff is who provisions the cert. | |
| 112 | + | |
| 113 | +## Deployment | |
| 114 | + | |
| 115 | +``` | |
| 116 | +deploy/ | |
| 117 | +├── kubernetes/ # :kubernetes image variant | |
| 118 | +│ ├── 00-namespace.yaml | |
| 119 | +│ ├── 10-rbac.yaml # ServiceAccount + Role for libcluster | |
| 120 | +│ ├── 20-postgres.yaml # baseline StatefulSet | |
| 121 | +│ ├── 30-secrets.example.yaml # DATABASE_URL, SECRET_KEY_BASE, … | |
| 122 | +│ ├── 40-certificate.yaml # cert-manager Certificate | |
| 123 | +│ ├── 50-services.yaml # headless / web / IRC LoadBalancer | |
| 124 | +│ ├── 60-deployment.yaml # 3 replicas, mounts cert Secret | |
| 125 | +│ └── 70-ingress.yaml | |
| 126 | +└── standalone/ # :standalone image variant | |
| 127 | + ├── docker-compose.yml + .env.example | |
| 128 | + └── quadlet/ # podman + systemd units | |
| 129 | + ├── ircd.network | |
| 130 | + ├── ircd-*.volume | |
| 131 | + ├── ircd*.container | |
| 132 | + ├── ircd.env.example | |
| 133 | + └── README.md | |
| 134 | +``` | |
| 135 | + | |
| 136 | +See `deploy/README.md` for the apply order and operating notes. | |
| 137 | + | |
| 138 | +## Architecture sketch | |
| 139 | + | |
| 140 | +``` | |
| 141 | + ┌─────────────────┐ | |
| 142 | + │ Horde Registry │ (cluster-wide; | |
| 143 | + │ │ {:channel,name} + | |
| 144 | + │ │ {:user,nick} keys) | |
| 145 | + └────────┬────────┘ | |
| 146 | + │ | |
| 147 | + ┌───────────────┐ ┌──────┴──────┐ ┌──────────────┐ | |
| 148 | + │ Channel pid │ │ User pid │ │ ChanServ pid │ | |
| 149 | + │ (singleton) │ │ (per conn) │ │ (singleton) │ | |
| 150 | + └───────┬───────┘ └──────┬──────┘ └──────────────┘ | |
| 151 | + │ │ | |
| 152 | + ▼ │ | |
| 153 | + ┌───────────────┐ │ | |
| 154 | + │ MessageLog │◀──────────┘ (PubSub fan-out | |
| 155 | + │ DeltaCrdt │ on channel topics) | |
| 156 | + │ (per node) │ | |
| 157 | + └───────────────┘ | |
| 158 | +``` | |
| 159 | + | |
| 160 | + * **Channel** owns membership, topic, modes, mask lists, in-memory | |
| 161 | + state. Mutations to non-prefix modes persist to | |
| 162 | + `channels.modes`; member access changes persist to | |
| 163 | + `channel_memberships`. | |
| 164 | + * **MessageLog** is one DeltaCrdt per node holding every channel's | |
| 165 | + PRIVMSG log keyed by `{channel, hlc}`. Nodes mesh via | |
| 166 | + `set_neighbours` (driven by `NodeListener`). The same drain path | |
| 167 | + fans out live messages and on-heal replay, so receivers don't | |
| 168 | + care whether something was just typed or just merged. | |
| 169 | + * **Connection** is a `ThousandIsland` handler per TCP socket. It | |
| 170 | + parses RFC-2812 frames, maintains per-conn state, subscribes to | |
| 171 | + PubSub channel topics, and registers itself in the Horde | |
| 172 | + registry under `{:user, downcased_nick}`. PINGs are | |
| 173 | + server-initiated on inactivity. | |
| 174 | + | |
| 175 | +## Build variants | |
| 176 | + | |
| 177 | +`.gitlab-ci.yml` runs two Kaniko jobs on push to `master`, publishing: | |
| 178 | + | |
| 179 | + * `$REGISTRY/oh_my_ex_oh_ircd:standalone-<sha>` (+ `:standalone-master`, | |
| 180 | + `:master`, `:latest`) | |
| 181 | + * `$REGISTRY/oh_my_ex_oh_ircd:kubernetes-<sha>` (+ `:kubernetes-master`) | |
| 182 | + | |
| 183 | +Both come from the same Dockerfile; the only difference is the | |
| 184 | +`TLS_MODE` build-arg, which the runtime config picks up via | |
| 185 | +`Application.compile_env`. | |
| 186 | + | |
| 187 | +## Tests | |
| 188 | + | |
| 189 | +```sh | |
| 190 | +mix test | |
| 191 | +``` | |
| 192 | + | |
| 193 | +`async: true` integration tests use `start_supervised!` to spin up | |
| 194 | +per-test `ThousandIsland` listeners on ephemeral ports; `async: false` | |
| 195 | +DB tests share the sandbox in `:shared` mode. The TLS handshake test | |
| 196 | +uses `SiteEncrypt.Phoenix.Test.clean_restart/1` to drive the internal | |
| 197 | +CA. Full suite is ~37s on a laptop. | |
| 198 | + | |
| 199 | +## Project layout | |
| 200 | + | |
| 201 | +``` | |
| 202 | +lib/oh_my_ex_oh_ircd/ # main app | |
| 203 | +├── accounts/ # phx.gen.auth | |
| 204 | +├── channels/ # ChannelRecord + ChannelMembership schemas | |
| 205 | +├── channels.ex # registration context | |
| 206 | +├── cluster/ # Horde + libcluster glue | |
| 207 | +├── irc/ # protocol, channel, connection, services | |
| 208 | +│ ├── auth.ex # SASL/PASS verification | |
| 209 | +│ ├── chan_serv.ex | |
| 210 | +│ ├── channel.ex # the singleton GenServer | |
| 211 | +│ ├── connection.ex # ThousandIsland handler | |
| 212 | +│ ├── hlc.ex # hybrid logical clock | |
| 213 | +│ ├── listener.ex # plain IRC listener (6667) | |
| 214 | +│ ├── message.ex # RFC parser/encoder | |
| 215 | +│ ├── message_log.ex # per-node DeltaCrdt | |
| 216 | +│ ├── services.ex # boot-time ChanServ starter | |
| 217 | +│ ├── stats.ex | |
| 218 | +│ └── tls_listener.ex # TLS IRC listener (6697) | |
| 219 | +└── themes.ex | |
| 220 | +lib/oh_my_ex_oh_ircd_web/ # Phoenix UI | |
| 221 | +└── live/home_live.ex # the LiveView landing | |
| 222 | +priv/repo/migrations/ # users + channels + channel_memberships | |
| 223 | +deploy/ # k8s + standalone manifests | |
| 224 | +``` | |
| 225 | + | |
| 226 | +## Learn more about the underpinnings | |
| 227 | + | |
| 228 | + * Phoenix Framework: <https://www.phoenixframework.org/> | |
| 229 | + * Horde: <https://github.com/derekkraan/horde> | |
| 230 | + * DeltaCrdt: <https://github.com/derekkraan/delta_crdt_ex> | |
| 231 | + * libcluster: <https://github.com/bitwalker/libcluster> | |
| 232 | + * site_encrypt: <https://github.com/sasa1977/site_encrypt> | |
| 233 | + * RFC 2812 (IRC): <https://datatracker.ietf.org/doc/html/rfc2812> | |
| 234 | + * IRCv3: <https://ircv3.net/> |
Parents: 30112b1