README

cac0cb2 · gmorell · 2026-05-19 04:16

1 files +227 -11

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