Registering a channel now needs the caller to actually hold operator
status in it, so you can't register a channel you don't control. The
network view tracks per-channel ops from FJOIN prefixes and FMODE +o/-o.
When a registration conflict takes an account name away from this node,
the channels it had registered locally as founder can't transfer to the
name's new owner, so they are dropped (releasing the registered mode) and
the former session is told which were released.
When a peer's registration wins a name a local session was logged into,
that session authenticated against a credential that no longer exists.
The node now clears its accountname (the ircd emits RPL_LOGGEDOUT) and
tells the user why. A new outbound channel lets the engine push these
services-initiated actions to the uplink from the gossip path, drained
by the link loop alongside incoming traffic.
Two nodes registering the same name before gossip converges no longer
end up disagreeing. Each account records its home node, and the merge
keeps the earliest registration (by timestamp, then home node), so every
node folds the log to the same owner regardless of delivery order. The
merge is commutative and idempotent.
IDENTIFY now takes an optional leading account name, so logging into an
account whose name differs from your current nick works. Without it, the
account still defaults to the current nick as before.
Each event now has a scope: account identity is global and gossips to
every peer; channel state is local to the node that authored it. The
version vector, push, digest and compaction only carry global events,
and ingest refuses any non-global entry a peer sends. A node can no
longer be handed ownership of a channel registered on a network it was
never part of.
Tracks channel membership (join/part/kick), the current key, and per-nick
last-seen in the network view. Entry messages greet joiners; enforce
re-applies the lock, access modes and auto-kicks to everyone present;
getkey reports the key; seen reports last activity; clone copies a
channel's settings; aop/sop/vop are tiered shortcuts over the access list.
SET <#channel> FOUNDER <account> transfers ownership; SET DESC <text>
stores a description shown in INFO. Both are founder-gated and persisted
to the event log.
Adds the remaining moderation and listing commands. Auto-kick masks are
stored in the event log and enforced on join: a matching user is banned
and kicked. Topic and invite are sourced from the ChanServ pseudoclient.
Adds channel moderation commands sourced from the ChanServ pseudoclient.
Tracks connected users' nicks and hosts so targets can be resolved by
nick and banned by *!*@host. All commands require operator access.
ACCESS <#channel> LIST|ADD|DEL manages op/voice by account (founder-only
changes). Entries are ChannelAccessAdd/Del events, so they replicate and
compact like the rest, and join grants +o/+v from the list.
MODE <#channel> <modes> lets the founder set channel modes through
ChanServ (sourced from ChanServ). The mode string is passed through, so
simple modes and masks work; founder-gated.
Channel modes (+r, mode lock, auto-op, enforcement) are sent from the
ChanServ pseudoclient instead of the services server, so users see
ChanServ as the setter. The engine locates the channel-managing service;
the FMODE filter now ignores anything under our SID prefix (server and
pseudoclients) so enforcement can't loop on our own changes.
Founders lock channel modes with MLOCK <#channel> [modes]. The lock is a
ChannelMlock event, so it replicates and compacts like everything else;
it is applied on channel creation alongside +r and enforced by reverting
any FMODE that breaks it. Our own changes are filtered so enforcement
can't loop. Simple (paramless) modes for now.
Clearer, more actionable wording across IDENTIFY, LOGOUT, HELP, the
unknown-command hint, and the registration replies, so the two services
read consistently.
INFO prints the founder and a UTC registration date instead of a raw
Unix timestamp (formatted with a small civil-date helper, no date crate),
and the notices are reworded to read more naturally.
fedserv now handles FJOIN (channel create/burst) and sets +r on any
channel that is registered, so a registered channel regains the mode
after it empties and is recreated, and after a services relink. Single
joins arrive as IJOIN and are ignored.
ChanServ sends FMODE to mark a registered channel +r (the m_services
c_registered mode), and -r on drop. Sourced from our server, which the
ircd requires for that mode, with a low TS so it applies to the existing
channel.
REGISTER/INFO/DROP with founder auth, stored as ChannelRegistered and
ChannelDropped events so channel registrations replicate across nodes
and compact like accounts. Registration requires an identified account;
only the founder can drop.
When a forwarder falls behind and drops pushes (broadcast lag), it sends
a digest with reply set; the peer answers with its own digest and we
re-send exactly what it is missing. Recovery no longer waits out a full
poll interval, so the 10s backstop rarely matters.
Optional [gossip.tls] wraps the peer link in TLS: each node presents a
certificate and requires the peer to present one signed by the configured
CA. scripts/gen-certs.sh builds a CA and node certs.
Once the log grows past ~3x the account count it is rewritten to one
AccountRegistered per account (churned cert events dropped), via a temp
file and rename. Cert replay is now idempotent, so a snapshot plus
re-delivered events converge and a peer syncing from a compacted node
still gets every account. Runs on a 30m timer.
A broadcast channel forwards each freshly committed entry straight to
connected peers, so a registration propagates in milliseconds. The
periodic digest drops to a 10s anti-entropy backstop.
Nodes run anti-entropy over a TCP link: each advertises its version
vector, the peer replies with the log entries it lacks, and ingest is
idempotent so re-delivery and reconnect after a split both converge.
The engine is shared behind a mutex; config gains [gossip] and [[peer]].
Entries carry a Lamport timestamp alongside origin/seq. EventLog keeps a
per-origin version vector and ingests peer entries idempotently.
Db::ingest folds them in. No transport yet.
Cover the login exchange end to end without an ircd: single-chunk,
non-400-multiple chunking, an exact-800 response ending on a lone "+",
and a bad password. Guards the base64 reassembly and the accountname
login the uplink turns into RPL_LOGGEDIN.
Turn the PLAIN verification exchange into a full account login:
- Advertise the mechanism list to the uplink as `saslmechlist` network
metadata, so clients see `sasl=PLAIN` in CAP LS (SASL 3.2).
- On a valid response set the client's `accountname` metadata (drives
RPL_LOGGEDIN 900) before reporting success (D S, drives 903); a bad
credential or unknown mechanism reports failure (D F, drives 904).
- Reassemble the base64 response from the uplink's 400-byte AUTHENTICATE
chunks, ending on a short chunk or a lone "+", with an upper bound to
cap a pre-auth client's buffer.
- Log the user in on NickServ REGISTER and IDENTIFY too, through the same
accountname metadata, and return the canonical account name from the
store so the login keeps its registered casing.
Verified against irctest server_tests/sasl.py (PLAIN success/failure,
non-ASCII, no-authzid, chunked payloads, unknown mechanism, too-long,
retry) on InspIRCd + fedserv.
fedserv now handles the SASL exchange an ircd relays over ENCAP (modes H/S/C/D):
on PLAIN it answers the empty challenge, decodes the client payload, and verifies
the credentials against the account store, replying D S or D F. Wiring the
account login on success and advertising the mechanism list to the ircd are the
remaining integration steps.
As the registration authority, fedserv accepts an account-registration request
relayed over the server link (ACCTREGISTER), creates the account through the
same store as classic NickServ REGISTER, and replies (ACCTREGRESULT) with
success or an error carrying the IRCv3 FAIL code — one account-creation path for
both the classic command and the draft/account-registration capability.
Accounts are an append-only JSONL event log (state is the fold of the log) with
argon2id-hashed passwords. NickServ gains REGISTER <password> [email] and
IDENTIFY <password>; the engine tracks UID->nick from the burst so a command
resolves to the sender's current nick. Classic and ircd-agnostic — the cap-based
account-registration forward is the next step.
The UID introduction omitted the second user field (insp sends both a real and a
displayed user), and the parser didn't strip IRCv3 message-tag prefixes, so
tagged PRIVMSGs addressed to a service were dropped. With both fixed a service
links to a real insp4 uplink and answers commands. Adds an irctest services
controller so this is verified against an isolated, throwaway ircd.
Protocol-agnostic core: a Protocol trait maps raw server-to-server lines to a
normalized event/action model, so the engine never touches a raw line and a new
ircd is one new module. Adds event-log state (state is a fold over the log) and
a service framework. First protocol impl links to an InspIRCd (insp4) uplink —
completes the CAPAB/SERVER handshake, bursts, and introduces NickServ.