Example network: leveva + ircd, linked, on Kubernetes#
A minimal two-server IRC network in the vibecoding namespace:
- leveva (
irc.example.net, SID0ABC) — the idiomatic-Rust daemon. - ircd (
hub.example.net, SID001A) — the faithful C-port daemon.
They are linked server-to-server. Each runs as its own Deployment + Service, and
both read their config from one ConfigMap (generated by kustomize from cfg/)
mounted at /cfg.
┌─────────────── namespace: vibecoding ───────────────┐
client → │ Service ircd:6667 ─→ ircd pod │
│ │ auto-connects (C-line) │
│ ▼ │
client → │ Service leveva:6667 ─→ leveva pod ←─ :6067 (S2S) │
└──────────────────────────────────────────────────────┘
Layout#
| File | Purpose |
|---|---|
kustomization.yaml |
namespace, resources, configMapGenerator, image pins |
namespace.yaml |
the vibecoding namespace |
leveva.yaml |
leveva Deployment + Service |
ircd.yaml |
ircd Deployment + Service |
cfg/leveva.kdl |
leveva config (KDL) |
cfg/ircd.conf |
ircd config (legacy pipe-delimited) |
cfg/leveva.motd |
leveva MOTD |
Build and push the images#
The images: block in kustomization.yaml remaps the bare names that
docker buildx bake produces to their registry location:
images:
- { name: leveva, newName: reg.xeiaso.net/shitpost/leveva, newTag: latest }
- { name: ircd, newName: reg.xeiaso.net/shitpost/ircd, newTag: latest }
So the cluster pulls reg.xeiaso.net/shitpost/{leveva,ircd}:latest. The bake file
has a REGISTRY variable that stamps that same prefix, so build + push in one
step from the repo root:
docker login reg.xeiaso.net
REGISTRY=reg.xeiaso.net/shitpost/ docker buildx bake --push
# → builds and pushes reg.xeiaso.net/shitpost/leveva:latest
# and reg.xeiaso.net/shitpost/ircd:latest
To point at a different registry/tag, edit the images: block (and pass a
matching REGISTRY=/TAG= to bake). The Deployments set imagePullPolicy: Always because the tag is :latest; pin a digest or version tag for
reproducible rollouts and drop that to IfNotPresent.
Private registry#
If reg.xeiaso.net/shitpost needs auth to pull, create a pull secret in the
namespace and reference it. The namespace must exist first:
kubectl create namespace vibecoding
kubectl -n vibecoding create secret docker-registry regcred \
--docker-server=reg.xeiaso.net \
--docker-username=<user> --docker-password=<token>
Then add it to both Deployments — either edit the pod specs to add
imagePullSecrets:
- name: regcred
or, cluster-wide for the namespace, patch the default ServiceAccount:
kubectl -n vibecoding patch serviceaccount default \
-p '{"imagePullSecrets":[{"name":"regcred"}]}'
Local cluster (no registry)#
To skip the registry on kind/minikube, build locally, load the images, and
override the names back to bare :latest (with IfNotPresent):
docker buildx bake # builds leveva:latest, ircd:latest
kind load docker-image leveva:latest ircd:latest
# minikube image load leveva:latest && minikube image load ircd:latest
Apply#
kubectl apply -k docs/k8s/example-network
kubectl -n vibecoding get pods,svc
Editing any file under cfg/ and re-applying rolls both Deployments
automatically — the ConfigMap name carries a content hash.
Verify the link#
# leveva should report the linked server:
kubectl -n vibecoding logs deploy/leveva | grep -i "S2S link established"
# or talk to it directly:
kubectl -n vibecoding port-forward svc/leveva 6667:6667
# then: nc localhost 6667 → NICK x / USER x x x x → /LINKS , /MAP
Which side dials, and why#
The ircd is the active side: its C-line has a port, so try_connections()
auto-dials leveva and retries on the server-class cadence (30 s). leveva is
passive — it only initiates outbound links from an operator /CONNECT, so here
it just listens on :6067 and authorises the inbound link in its connect
block. The passwords mirror across the two configs:
- ircd
C-line passwordhubpass== levevaaccept-password - leveva
send-passwordlevevapass== ircdN-line password
Caveat: the C ircd is IPv6-only#
The C port's sockets are AF_INET6, so when it auto-connects it resolves the
leveva Service via an AAAA record. That works on a dual-stack or IPv6
cluster (the leveva Service is set ipFamilyPolicy: PreferDualStack). On a
single-stack IPv4 cluster the Service has no AAAA, and the auto-connect can't
resolve a destination. Options there:
- Enable dual-stack on the cluster (then the Service gets an IPv6 ClusterIP).
- Pin the leveva Service to a known IPv6
clusterIPand put that literal in the ircdC/Nlines instead of the DNS name (isdigit-leading hosts go straight throughinetpton(AF_INET6, …), skipping resolution entirely).
Reversing the dial direction (leveva → ircd via /CONNECT) doesn't help here:
the ircd C/N host fields double as the accept mask, and they're set to
leveva's Service name — which resolves to the ClusterIP, not the leveva pod's
source IP — so ircd would reject the inbound match. If you want that direction,
widen the ircd C/N host masks to * (it can no longer auto-dial then, since
that same field is the dial target).
This example is intentionally plaintext (no TLS, no iauth ident probe) to keep it readable — don't ship the passwords as-is.