serverless-ircd 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +28 -0
- package/.github/workflows/deploy-cf-tcp.yml +26 -2
- package/.github/workflows/deploy-cf.yml +26 -0
- package/CHANGELOG.md +289 -0
- package/README.md +153 -20
- package/apps/aws-stack/bin/aws.ts +36 -0
- package/apps/aws-stack/package.json +1 -1
- package/apps/aws-stack/src/aws-stack.ts +221 -15
- package/apps/aws-stack/tests/stack.test.ts +450 -16
- package/apps/cf-tcp-container/Dockerfile +37 -5
- package/apps/cf-tcp-container/package.json +7 -2
- package/apps/cf-tcp-container/src/config-loader.ts +113 -2
- package/apps/cf-tcp-container/src/container-server.ts +256 -79
- package/apps/cf-tcp-container/src/main.ts +22 -7
- package/apps/cf-tcp-container/src/proxy-protocol.ts +112 -0
- package/apps/cf-tcp-container/terraform/spectrum.tf +40 -11
- package/apps/cf-tcp-container/tests/config-loader.test.ts +170 -0
- package/apps/cf-tcp-container/tests/container-server-tls.test.ts +382 -0
- package/apps/cf-tcp-container/tests/container-server.test.ts +358 -31
- package/apps/cf-tcp-container/tests/dockerfile.test.ts +110 -0
- package/apps/cf-tcp-container/tests/proxy-protocol.test.ts +187 -0
- package/apps/cf-tcp-container/tests/spectrum-terraform.test.ts +135 -0
- package/apps/cf-tcp-container/tests/tls-e2e.test.ts +5 -1
- package/apps/cf-tcp-container/wrangler.toml +17 -4
- package/apps/cf-worker/package.json +2 -2
- package/apps/cf-worker/src/worker.ts +77 -5
- package/apps/cf-worker/tests/raw-modules.d.ts +11 -0
- package/apps/cf-worker/tests/smoke.test.ts +4 -0
- package/apps/cf-worker/tests/wrangler-config.test.ts +47 -0
- package/apps/cf-worker/tests/ws-admission.test.ts +112 -0
- package/apps/cf-worker/tests/ws-rate-limit.test.ts +133 -0
- package/apps/cf-worker/wrangler.test.toml +15 -1
- package/apps/cf-worker/wrangler.toml +86 -9
- package/apps/local-cli/package.json +1 -1
- package/apps/local-cli/src/config-loader.ts +14 -2
- package/apps/local-cli/src/line-scanner.ts +26 -0
- package/apps/local-cli/src/server.ts +23 -2
- package/apps/local-cli/tests/line-scanner.test.ts +64 -0
- package/apps/local-cli/tests/tcp.test.ts +29 -0
- package/apps/web/package.json +1 -1
- package/docs/AWS-Deployment.md +123 -22
- package/docs/AWS-TCP-Deployment.md +37 -2
- package/docs/Chat-History.md +55 -0
- package/docs/Cloudflare-Deployment-Guide.md +9 -2
- package/docs/Cloudflare-TCP-Deployment.md +135 -52
- package/docs/SASL-EXTERNAL.md +175 -0
- package/package.json +3 -3
- package/packages/aws-adapter/package.json +1 -1
- package/packages/aws-adapter/src/admission.ts +28 -13
- package/packages/aws-adapter/src/aws-runtime.ts +30 -3
- package/packages/aws-adapter/src/cdk-table-defs.ts +34 -6
- package/packages/aws-adapter/src/config-loader.ts +134 -6
- package/packages/aws-adapter/src/dynamo-services-store.ts +12 -0
- package/packages/aws-adapter/src/handlers/connect.ts +47 -1
- package/packages/aws-adapter/src/handlers/default.ts +95 -6
- package/packages/aws-adapter/src/handlers/index.ts +31 -2
- package/packages/aws-adapter/src/handlers/nlb-stream.ts +132 -8
- package/packages/aws-adapter/src/ip-admission.ts +79 -0
- package/packages/aws-adapter/src/serialize.ts +8 -0
- package/packages/aws-adapter/src/tables.ts +9 -0
- package/packages/aws-adapter/tests/admission.test.ts +60 -2
- package/packages/aws-adapter/tests/aws-harness.ts +23 -1
- package/packages/aws-adapter/tests/aws-runtime.test.ts +64 -0
- package/packages/aws-adapter/tests/config-loader.test.ts +151 -0
- package/packages/aws-adapter/tests/connect.test.ts +199 -2
- package/packages/aws-adapter/tests/default-frame-limit.test.ts +231 -0
- package/packages/aws-adapter/tests/default-occ.test.ts +10 -3
- package/packages/aws-adapter/tests/dynamo-services-store-unit.test.ts +123 -1
- package/packages/aws-adapter/tests/handlers.test.ts +57 -1
- package/packages/aws-adapter/tests/nlb-secure.test.ts +362 -0
- package/packages/aws-adapter/tests/nlb-stream.test.ts +628 -9
- package/packages/cf-adapter/package.json +1 -1
- package/packages/cf-adapter/src/cf-runtime.ts +48 -9
- package/packages/cf-adapter/src/config-loader.ts +133 -8
- package/packages/cf-adapter/src/connection-do.ts +154 -21
- package/packages/cf-adapter/src/counter-do.ts +142 -0
- package/packages/cf-adapter/src/d1-services-store.ts +47 -5
- package/packages/cf-adapter/src/env.ts +88 -0
- package/packages/cf-adapter/src/index.ts +17 -1
- package/packages/cf-adapter/src/rate-limit-do.ts +87 -0
- package/packages/cf-adapter/tests/cf-runtime.test.ts +104 -15
- package/packages/cf-adapter/tests/config-loader.test.ts +159 -0
- package/packages/cf-adapter/tests/connection-do-counter.test.ts +165 -0
- package/packages/cf-adapter/tests/connection-do-frame-limit.test.ts +177 -0
- package/packages/cf-adapter/tests/connection-do-pure.test.ts +74 -5
- package/packages/cf-adapter/tests/connection-do-ws-spec-contract.test.ts +7 -4
- package/packages/cf-adapter/tests/counter-do.test.ts +181 -0
- package/packages/cf-adapter/tests/d1-services-store.test.ts +192 -1
- package/packages/cf-adapter/tests/rate-limit-do.test.ts +160 -0
- package/packages/cf-adapter/tests/worker/main.ts +4 -0
- package/packages/cf-adapter/wrangler.test.toml +18 -1
- package/packages/in-memory-runtime/package.json +1 -1
- package/packages/in-memory-runtime/src/in-memory-runtime.ts +25 -0
- package/packages/in-memory-runtime/tests/in-memory-runtime.test.ts +74 -0
- package/packages/irc-core/package.json +1 -1
- package/packages/irc-core/src/caps/capabilities.ts +20 -10
- package/packages/irc-core/src/certfp.ts +178 -0
- package/packages/irc-core/src/commands/cap.ts +10 -2
- package/packages/irc-core/src/commands/chanserv.ts +117 -14
- package/packages/irc-core/src/commands/chathistory.ts +13 -5
- package/packages/irc-core/src/commands/hostserv.ts +84 -8
- package/packages/irc-core/src/commands/index.ts +2 -1
- package/packages/irc-core/src/commands/invite.ts +1 -7
- package/packages/irc-core/src/commands/join.ts +1 -16
- package/packages/irc-core/src/commands/kick.ts +1 -8
- package/packages/irc-core/src/commands/list.ts +1 -8
- package/packages/irc-core/src/commands/mode.ts +1 -8
- package/packages/irc-core/src/commands/multiline.ts +4 -10
- package/packages/irc-core/src/commands/names.ts +53 -13
- package/packages/irc-core/src/commands/nickserv.ts +40 -1
- package/packages/irc-core/src/commands/oper.ts +361 -8
- package/packages/irc-core/src/commands/part.ts +4 -10
- package/packages/irc-core/src/commands/privmsg.ts +8 -4
- package/packages/irc-core/src/commands/registration.ts +146 -2
- package/packages/irc-core/src/commands/sasl.ts +136 -19
- package/packages/irc-core/src/commands/topic.ts +10 -12
- package/packages/irc-core/src/commands/who.ts +1 -8
- package/packages/irc-core/src/config.ts +393 -20
- package/packages/irc-core/src/effects.ts +24 -0
- package/packages/irc-core/src/flood-control.ts +10 -10
- package/packages/irc-core/src/frame-rate-limit.ts +82 -0
- package/packages/irc-core/src/index.ts +8 -0
- package/packages/irc-core/src/oper-hashing.ts +43 -0
- package/packages/irc-core/src/oper-lockout.ts +87 -0
- package/packages/irc-core/src/ports.ts +395 -36
- package/packages/irc-core/src/protocol/bytes.ts +65 -0
- package/packages/irc-core/src/protocol/channel-name.ts +37 -0
- package/packages/irc-core/src/protocol/index.ts +12 -1
- package/packages/irc-core/src/protocol/outbound.ts +43 -10
- package/packages/irc-core/src/protocol/parser.ts +79 -10
- package/packages/irc-core/src/state/connection.ts +13 -0
- package/packages/irc-core/src/types.ts +228 -13
- package/packages/irc-core/src/ws-framing.ts +5 -4
- package/packages/irc-core/tests/bytes.test.ts +89 -0
- package/packages/irc-core/tests/certfp.test.ts +117 -0
- package/packages/irc-core/tests/commands/cap.test.ts +76 -2
- package/packages/irc-core/tests/commands/chanserv.test.ts +166 -0
- package/packages/irc-core/tests/commands/chathistory.test.ts +140 -0
- package/packages/irc-core/tests/commands/hostserv.test.ts +316 -0
- package/packages/irc-core/tests/commands/join.test.ts +78 -1
- package/packages/irc-core/tests/commands/names.test.ts +193 -0
- package/packages/irc-core/tests/commands/nickserv.test.ts +182 -2
- package/packages/irc-core/tests/commands/oper.test.ts +560 -2
- package/packages/irc-core/tests/commands/privmsg.test.ts +16 -0
- package/packages/irc-core/tests/commands/registration.test.ts +463 -1
- package/packages/irc-core/tests/commands/sasl.test.ts +596 -7
- package/packages/irc-core/tests/commands/topic.test.ts +137 -2
- package/packages/irc-core/tests/commands/unified-account.test.ts +2 -0
- package/packages/irc-core/tests/config.test.ts +534 -2
- package/packages/irc-core/tests/effects.test.ts +14 -0
- package/packages/irc-core/tests/flood-control.test.ts +29 -1
- package/packages/irc-core/tests/frame-rate-limit.test.ts +98 -0
- package/packages/irc-core/tests/oper-hashing.test.ts +60 -0
- package/packages/irc-core/tests/oper-lockout.test.ts +74 -0
- package/packages/irc-core/tests/outbound.test.ts +148 -0
- package/packages/irc-core/tests/parser.test.ts +287 -5
- package/packages/irc-core/tests/persistent-services-store.test.ts +141 -0
- package/packages/irc-core/tests/ports.test.ts +99 -7
- package/packages/irc-core/tests/services-store.test.ts +376 -14
- package/packages/irc-core/tests/ws-framing.test.ts +45 -0
- package/packages/irc-server/package.json +1 -1
- package/packages/irc-server/src/actor.ts +123 -8
- package/packages/irc-server/src/dispatch.ts +1 -0
- package/packages/irc-server/src/index.ts +7 -0
- package/packages/irc-server/src/redact.ts +159 -0
- package/packages/irc-server/src/runtime.ts +14 -0
- package/packages/irc-server/src/transport.ts +28 -1
- package/packages/irc-server/tests/actor.test.ts +544 -7
- package/packages/irc-server/tests/dispatch.test.ts +31 -0
- package/packages/irc-server/tests/redact.test.ts +198 -0
- package/packages/irc-server/tests/runtime.test.ts +2 -0
- package/packages/irc-server/tests/transport.test.ts +66 -0
- package/packages/irc-test-support/package.json +1 -1
- package/packages/irc-test-support/src/in-memory-harness.ts +4 -0
- package/scripts/package.json +1 -1
- package/tools/ci-hardening/package.json +2 -2
- package/tools/ci-hardening/src/cf-deploy-cli.ts +3 -0
- package/tools/ci-hardening/src/cf-deploy.ts +118 -0
- package/tools/ci-hardening/src/deploy-hostname.ts +118 -0
- package/tools/ci-hardening/src/env-var-drift.ts +192 -0
- package/tools/ci-hardening/src/hostname-guard.ts +11 -0
- package/tools/ci-hardening/src/index.ts +17 -0
- package/tools/ci-hardening/tests/__wrangler_missing__.toml +2 -0
- package/tools/ci-hardening/tests/__wrangler_placeholder__.toml +3 -0
- package/tools/ci-hardening/tests/__wrangler_real__.toml +3 -0
- package/tools/ci-hardening/tests/cf-deploy.test.ts +200 -0
- package/tools/ci-hardening/tests/deploy-hostname.test.ts +348 -0
- package/tools/ci-hardening/tests/env-var-drift.test.ts +284 -0
- package/tools/ci-hardening/vitest.config.ts +5 -1
- package/tools/hash-oper-cred.ts +85 -0
- package/tools/load-test/package.json +1 -1
- package/tools/tcp-ws-forwarder/package.json +1 -1
package/.github/workflows/ci.yml
CHANGED
|
@@ -124,6 +124,34 @@ jobs:
|
|
|
124
124
|
# config) before it reaches main.
|
|
125
125
|
run: pnpm --filter @serverless-ircd/ci-hardening test
|
|
126
126
|
|
|
127
|
+
- name: Container non-root smoke (gated on docker availability)
|
|
128
|
+
# Live proof that the cf-tcp-container image drops root: the
|
|
129
|
+
# runtime UID must be 1000 and /data must be writable by it.
|
|
130
|
+
# The static Dockerfile assertions in
|
|
131
|
+
# apps/cf-tcp-container/tests/dockerfile.test.ts always run in
|
|
132
|
+
# the test step; this smoke additionally builds and runs the
|
|
133
|
+
# real image. Gated because the self-hosted runner image ships
|
|
134
|
+
# without a docker daemon (see the DynamoDB Local notes above).
|
|
135
|
+
run: |
|
|
136
|
+
if ! command -v docker >/dev/null 2>&1 || ! timeout 15 docker info >/dev/null 2>&1; then
|
|
137
|
+
echo "docker not available — skipping live container smoke (static Dockerfile test covers it)"
|
|
138
|
+
exit 0
|
|
139
|
+
fi
|
|
140
|
+
docker build -f apps/cf-tcp-container/Dockerfile -t sirc-tcp-origin-smoke .
|
|
141
|
+
uid="$(docker run --rm sirc-tcp-origin-smoke id -u)"
|
|
142
|
+
echo "runtime UID: ${uid}"
|
|
143
|
+
if [ "${uid}" != "1000" ]; then
|
|
144
|
+
echo "FAIL: expected runtime UID 1000, got ${uid}"
|
|
145
|
+
exit 1
|
|
146
|
+
fi
|
|
147
|
+
docker run --rm sirc-tcp-origin-smoke \
|
|
148
|
+
node -e "const fs=require('fs');fs.writeFileSync('/data/.smoke','ok');fs.unlinkSync('/data/.smoke')"
|
|
149
|
+
if [ "$?" -ne 0 ]; then
|
|
150
|
+
echo "FAIL: /data not writable as UID 1000"
|
|
151
|
+
exit 1
|
|
152
|
+
fi
|
|
153
|
+
echo "SMOKE PASS: image runs as UID 1000 and /data is writable"
|
|
154
|
+
|
|
127
155
|
- name: Upload coverage
|
|
128
156
|
if: always()
|
|
129
157
|
uses: actions/upload-artifact@v3
|
|
@@ -17,9 +17,22 @@ name: deploy-cf-tcp
|
|
|
17
17
|
# CLOUDFLARE_ACCOUNT_ID Account ID for the target zone.
|
|
18
18
|
# CF_ZONE_ID Zone ID for the Spectrum hostname.
|
|
19
19
|
# CF_TCP_ORIGIN_ADDRESS Hostname/IP of the container origin.
|
|
20
|
+
#
|
|
21
|
+
# Inputs (prompted on workflow_dispatch):
|
|
22
|
+
# hostname REQUIRED. Public IRC hostname this deploy
|
|
23
|
+
# serves (e.g. irc.example.net). The pipeline
|
|
24
|
+
# fails loudly when unset and refuses to ship
|
|
25
|
+
# while it is still a repo placeholder
|
|
26
|
+
# (irc.example.com / irc.your-domain.invalid)
|
|
27
|
+
# — the guard step below enforces both.
|
|
20
28
|
|
|
21
29
|
on:
|
|
22
30
|
workflow_dispatch:
|
|
31
|
+
inputs:
|
|
32
|
+
hostname:
|
|
33
|
+
description: 'Public IRC hostname for this deploy (must not be a placeholder)'
|
|
34
|
+
required: true
|
|
35
|
+
type: string
|
|
23
36
|
|
|
24
37
|
concurrency:
|
|
25
38
|
group: cf-tcp-deploy
|
|
@@ -49,6 +62,16 @@ jobs:
|
|
|
49
62
|
- name: Build workspace
|
|
50
63
|
run: pnpm build
|
|
51
64
|
|
|
65
|
+
# Fail fast, before any deploy or terraform step, when the
|
|
66
|
+
# hostname input is unset or still a repo placeholder. Runs the
|
|
67
|
+
# same guard the deploy scripts use (tools/ci-hardening
|
|
68
|
+
# hostname-guard); the `::error::` message names the offending
|
|
69
|
+
# value and how to set a real hostname.
|
|
70
|
+
- name: 'Guard: reject placeholder hostnames'
|
|
71
|
+
env:
|
|
72
|
+
DEPLOY_HOSTNAME: ${{ inputs.hostname }}
|
|
73
|
+
run: node tools/ci-hardening/dist/hostname-guard.js "$DEPLOY_HOSTNAME"
|
|
74
|
+
|
|
52
75
|
- name: Typecheck
|
|
53
76
|
run: pnpm typecheck
|
|
54
77
|
|
|
@@ -60,14 +83,15 @@ jobs:
|
|
|
60
83
|
env:
|
|
61
84
|
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
62
85
|
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
|
63
|
-
|
|
86
|
+
SERVER_NAME: ${{ inputs.hostname }}
|
|
87
|
+
run: pnpm run deploy -- --var "SERVER_NAME:$SERVER_NAME"
|
|
64
88
|
|
|
65
89
|
- name: Apply Spectrum TLS config
|
|
66
90
|
working-directory: apps/cf-tcp-container/terraform
|
|
67
91
|
env:
|
|
68
92
|
TF_VAR_zone_id: ${{ secrets.CF_ZONE_ID }}
|
|
69
93
|
TF_VAR_origin_address: ${{ secrets.CF_TCP_ORIGIN_ADDRESS }}
|
|
70
|
-
TF_VAR_hostname:
|
|
94
|
+
TF_VAR_hostname: ${{ inputs.hostname }}
|
|
71
95
|
run: |
|
|
72
96
|
terraform init
|
|
73
97
|
terraform apply -auto-approve
|
|
@@ -24,9 +24,23 @@ name: deploy-cf
|
|
|
24
24
|
# plus the deployer's account subdomain. Override by setting
|
|
25
25
|
# `CF_SMOKE_URL` in the repo Variables store when custom routes are
|
|
26
26
|
# wired.
|
|
27
|
+
#
|
|
28
|
+
# Inputs (prompted on workflow_dispatch):
|
|
29
|
+
# hostname REQUIRED. Public IRC hostname this deploy serves
|
|
30
|
+
# (e.g. irc.example.net). Fails loudly when unset and
|
|
31
|
+
# refuses to ship while it is still a repo placeholder
|
|
32
|
+
# (irc.example.com / irc.your-domain.invalid) — the
|
|
33
|
+
# guard step below enforces both, and the deploy step
|
|
34
|
+
# overrides the shipped wrangler.toml `SERVER_NAME`
|
|
35
|
+
# default with the input via `--var`.
|
|
27
36
|
|
|
28
37
|
on:
|
|
29
38
|
workflow_dispatch:
|
|
39
|
+
inputs:
|
|
40
|
+
hostname:
|
|
41
|
+
description: 'Public IRC hostname for this deploy (must not be a placeholder)'
|
|
42
|
+
required: true
|
|
43
|
+
type: string
|
|
30
44
|
|
|
31
45
|
# Prevent two deploys racing the same DO namespace.
|
|
32
46
|
concurrency:
|
|
@@ -57,6 +71,14 @@ jobs:
|
|
|
57
71
|
- name: Build workspace
|
|
58
72
|
run: pnpm build
|
|
59
73
|
|
|
74
|
+
# Fail fast, before the deploy step, when the hostname input is
|
|
75
|
+
# unset or still a repo placeholder (same guard the deploy
|
|
76
|
+
# scripts use; tools/ci-hardening hostname-guard).
|
|
77
|
+
- name: 'Guard: reject placeholder hostnames'
|
|
78
|
+
env:
|
|
79
|
+
DEPLOY_HOSTNAME: ${{ inputs.hostname }}
|
|
80
|
+
run: node tools/ci-hardening/dist/hostname-guard.js "$DEPLOY_HOSTNAME"
|
|
81
|
+
|
|
60
82
|
- name: Typecheck
|
|
61
83
|
run: pnpm typecheck
|
|
62
84
|
|
|
@@ -68,6 +90,10 @@ jobs:
|
|
|
68
90
|
env:
|
|
69
91
|
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
70
92
|
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
|
93
|
+
SERVER_NAME: ${{ inputs.hostname }}
|
|
94
|
+
# The guarded cf-deploy wrapper resolves SERVER_NAME from the env
|
|
95
|
+
# (set above from the workflow input), runs the placeholder guard,
|
|
96
|
+
# and injects it into wrangler as --var SERVER_NAME:<hostname>.
|
|
71
97
|
run: pnpm run deploy
|
|
72
98
|
|
|
73
99
|
- name: Resolve smoke URL
|
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,295 @@ For the release process itself — versioning policy, pre-release checklist,
|
|
|
10
10
|
cutting a tag, rolling back — see [`docs/Release-Process.md`](docs/Release-Process.md).
|
|
11
11
|
Cross-reference `progress.md` / `tickets.md` for per-ticket detail.
|
|
12
12
|
|
|
13
|
+
## [0.11.0] - 2026-08-18
|
|
14
|
+
|
|
15
|
+
This is the **security-audit remediation release**. The 2026-08-11
|
|
16
|
+
static audit (`SECURITY-REPORT.md`, 43 findings) is now fully
|
|
17
|
+
remediated: the Critical + first High findings (APIGW body logging,
|
|
18
|
+
OIDC-only deploys) and the CSPRNG id fix shipped in v0.10.0; the
|
|
19
|
+
remaining **40 findings (TICKET-182 … TICKET-222)** land here as a
|
|
20
|
+
layered hardening pass over every edge: **admission control and
|
|
21
|
+
rate limiting at every transport boundary** (global cap, per-IP
|
|
22
|
+
caps, connect-rate windows, per-connection frame budgets, bounded
|
|
23
|
+
line buffers), **credential hardening** (scrypt oper credentials,
|
|
24
|
+
constant-time / timing-equalized comparisons, brute-force lockouts
|
|
25
|
+
on every auth surface), **SASL `EXTERNAL` rebuilt behind an
|
|
26
|
+
operator opt-in with DER-fingerprint account binding**, and a sweep
|
|
27
|
+
of **protocol budget correctness fixes** (UTF-8 byte budgets, tag
|
|
28
|
+
caps, bare-CR rejection).
|
|
29
|
+
|
|
30
|
+
### Added — Admission control & rate limiting (`irc-core`, `cf-adapter`, `aws-adapter`, `aws-stack`)
|
|
31
|
+
|
|
32
|
+
Layered throttling of connection establishment and frame intake,
|
|
33
|
+
enforced at every transport edge (WS upgrade, NLB stream, TCP
|
|
34
|
+
origin) with `429` rejections.
|
|
35
|
+
|
|
36
|
+
- **Global `MAX_CLIENTS` cap with live-connection counter.**
|
|
37
|
+
Cloudflare reserves a slot in a `CounterDO` counter before
|
|
38
|
+
forwarding an upgrade and answers `429` (`ERROR :Closing link:
|
|
39
|
+
server full`) once the cap is reached; slots are released on
|
|
40
|
+
close and TTL-reaped if a DO dies without one. AWS keeps the
|
|
41
|
+
O(1) atomic counter from v0.8.0, now enforced against the
|
|
42
|
+
configured cap.
|
|
43
|
+
- **Per-IP connection caps + sliding-window connect-rate limits.**
|
|
44
|
+
`MAX_CONNECTIONS_PER_IP` (default 10) caps simultaneous
|
|
45
|
+
connections per source IP; `PER_IP_CONNECTION_RATE_MAX` /
|
|
46
|
+
`PER_IP_CONNECTION_RATE_WINDOW_MS` (default 5 / 60 s) cap new
|
|
47
|
+
establishments per window. Cloudflare enforces both at the worker
|
|
48
|
+
edge via a new `RateLimitDO` (sliding-window admission timestamps
|
|
49
|
+
keyed on `CF-Connecting-IP`; rejections never consume budget, so
|
|
50
|
+
a blocked IP recovers after window decay). AWS `$connect` counts
|
|
51
|
+
live + in-window rows via a new `sourceIp+connectedSince` GSI on
|
|
52
|
+
`Connections`, the NLB path applies the same budget to new flows
|
|
53
|
+
(first chunk only, so active sessions are not starved), API
|
|
54
|
+
Gateway stage throttling (rate + burst parameters) backstops
|
|
55
|
+
globally, and an **opt-in WAFv2 per-IP rate rule**
|
|
56
|
+
(`-c wafConnectRateLimit=<n>`) sits at the edge.
|
|
57
|
+
- **Per-connection inbound frame-rate window.**
|
|
58
|
+
`MAX_FRAMES_PER_WINDOW` / `FRAME_WINDOW_SECONDS` (default 50 /
|
|
59
|
+
5 s) enforced at the adapter boundary **before** the actor or any
|
|
60
|
+
storage write — a cheap early rejection for frame floods.
|
|
61
|
+
- **Bounded line-buffer memory.** TCP input buffers are capped at
|
|
62
|
+
**8 KiB** at every transport edge (local CLI, container origin,
|
|
63
|
+
NLB handler, forwarder), and the NLB handler rejects oversized
|
|
64
|
+
payloads outright — an unauthenticated peer can no longer pin
|
|
65
|
+
memory with an unterminated byte stream.
|
|
66
|
+
|
|
67
|
+
### Added — Cloudflare TCP origin hardening (`cf-tcp-container`)
|
|
68
|
+
|
|
69
|
+
The Spectrum origin is rebuilt as a hardened TLS terminator.
|
|
70
|
+
|
|
71
|
+
- **TLS terminates at the origin.** Spectrum switches to
|
|
72
|
+
`tls_mode = "full"` (encrypted all the way to the container, no
|
|
73
|
+
plaintext hop) and the container itself terminates TLS, pinned to
|
|
74
|
+
**TLS 1.2/1.3**.
|
|
75
|
+
- **PROXY protocol v1 is required on every flow.** The container
|
|
76
|
+
parses the `PROXY TCP4/TCP6 …` header (incremental, 107-byte
|
|
77
|
+
budget) to recover the real client IP — and **rejects flows
|
|
78
|
+
without one** — so per-IP admission (above) cannot be spoofed by
|
|
79
|
+
connecting directly to the origin. Origin ingress is locked to
|
|
80
|
+
Spectrum's published IP ranges in the Terraform.
|
|
81
|
+
- **Non-root runtime.** The container runs as the non-root `node`
|
|
82
|
+
user (UID 1000); the Dockerfile drops to an unprivileged user and
|
|
83
|
+
the cert files are owned by it.
|
|
84
|
+
|
|
85
|
+
### Added — AWS stack hardening (`aws-stack`, `aws-adapter`)
|
|
86
|
+
|
|
87
|
+
- **State tables protected.** Every DynamoDB state table
|
|
88
|
+
(`Connections`, `Nicks`, `Channels`, `Services`, …) now carries
|
|
89
|
+
`RETAIN` deletion policy, **deletion protection**, and
|
|
90
|
+
**point-in-time recovery (PITR)** — a stray `cdk destroy` or a
|
|
91
|
+
compromised console session can no longer silently destroy
|
|
92
|
+
persistent IRC state.
|
|
93
|
+
- **NLB stream path integrity.** The streaming handler asserts the
|
|
94
|
+
flow is TLS-secured (surfacing user mode `S`), **rejects flows
|
|
95
|
+
with missing or malformed `x-forwarded-for` / `x-forwarded-port`
|
|
96
|
+
headers**, and its Lambda now holds the
|
|
97
|
+
`execute-api:ManageConnections` grant against the management API
|
|
98
|
+
stage (fixes silent fanout failure on the TCP path).
|
|
99
|
+
- **APIGW logging role least-privilege.** The CloudWatch logging
|
|
100
|
+
role's policy is scoped to exact write-only grants on the
|
|
101
|
+
API-specific log groups.
|
|
102
|
+
|
|
103
|
+
### Added — Credential hardening (`irc-core`)
|
|
104
|
+
|
|
105
|
+
- **Scrypt-hashed oper credentials.** `OPER` verifies against a
|
|
106
|
+
scrypt hash + salt (`OPER_SALT` + `OPER_HASH`) with a timing-safe
|
|
107
|
+
compare; a new `tools/hash-oper-cred.ts` generates the pair
|
|
108
|
+
(`node --import tsx tools/hash-oper-cred.ts --user admin --stdin`).
|
|
109
|
+
The plaintext `OPER_PASSWORD` form still works but logs a
|
|
110
|
+
deprecation warning and will be removed.
|
|
111
|
+
- **Constant-time server-password comparison.** The
|
|
112
|
+
`SERVER_PASSWORD` gate compares in constant time.
|
|
113
|
+
- **Timing-equalized nick verification (no account enumeration).**
|
|
114
|
+
`verifyNick` runs a dummy scrypt verify for unknown accounts, so
|
|
115
|
+
response timing cannot distinguish "no such nick" from "wrong
|
|
116
|
+
password". NickServ `INFO` / `SET PASSWORD` replies are already
|
|
117
|
+
indistinguishable on the same axis.
|
|
118
|
+
- **Brute-force lockouts on every auth surface.** A per-connection
|
|
119
|
+
**SASL failure lockout** (repeated `904`s disconnect with "too
|
|
120
|
+
many failed SASL attempts"), a **per-IP `OPER` failure lockout**
|
|
121
|
+
(with oper-broadcast on trip), and a **per-account NickServ
|
|
122
|
+
`IDENTIFY` freeze**. The `AUTHENTICATE` **flood-control exemption
|
|
123
|
+
is removed** — SASL frames now count against the connection's
|
|
124
|
+
message bucket, closing the timing-oracle bandwidth amplifier.
|
|
125
|
+
- **Credential redaction from parse-error logs.** Parse-error log
|
|
126
|
+
records carry only a truncated leading token, the length, and a
|
|
127
|
+
stable reason code — never the raw line (a malformed line could
|
|
128
|
+
otherwise smuggle `PASS <secret>` past the parser into logs).
|
|
129
|
+
Workers log sampling is lowered to **10%** by default.
|
|
130
|
+
|
|
131
|
+
### Changed — SASL `EXTERNAL` (mTLS) rebuilt behind an operator opt-in (`irc-core`, adapters)
|
|
132
|
+
|
|
133
|
+
EXTERNAL is now advertised and accepted only when **all three**
|
|
134
|
+
hold: a bound mTLS identity source (CF API Shield / APIGW client
|
|
135
|
+
certs), the operator opt-in **`EXTERNAL_ENABLED`** (`true`/`1`,
|
|
136
|
+
**default off** — surfaced via both adapters' config loaders), and
|
|
137
|
+
a secure (TLS) connection. The `sasl` cap value and the
|
|
138
|
+
`AUTHENTICATE` reducer share the same gate so advertisement and
|
|
139
|
+
enforcement cannot drift; refusals answer `908 ERR_SASLMECHS`
|
|
140
|
+
listing `PLAIN` only and do not count toward the SASL lockout.
|
|
141
|
+
|
|
142
|
+
- **Accounts bind to the certificate's DER SHA-256 fingerprint**
|
|
143
|
+
(`fp:<hex>` entries in `RegisteredNick.certSubjects` — what
|
|
144
|
+
Cloudflare surfaces from `request.cf.tlsClientAuth`; the preferred
|
|
145
|
+
binding) **or a canonical subject DN** (the only identifier API
|
|
146
|
+
Gateway exposes). DNs are canonicalised before comparison —
|
|
147
|
+
attribute types lower-cased, whitespace collapsed, RDN/AVA
|
|
148
|
+
components sorted — so semantically equal DNs written differently
|
|
149
|
+
(including across cert re-issuance) still match. Legacy verbatim
|
|
150
|
+
bindings verify and are rewritten to the canonical spelling on
|
|
151
|
+
first use.
|
|
152
|
+
- **Transports without a client-cert surface pin the mechanism off.**
|
|
153
|
+
The `cf-tcp-container` Spectrum origin (and the NLB stream handler
|
|
154
|
+
unless an mTLS provider is injected) reject
|
|
155
|
+
`AUTHENTICATE EXTERNAL` outright with a transport-specific
|
|
156
|
+
`904 ERR_SASLFAIL` instead of silently mis-binding.
|
|
157
|
+
|
|
158
|
+
### Changed — Protocol budget correctness (`irc-core`, `irc-server`)
|
|
159
|
+
|
|
160
|
+
- **All length budgets are enforced in UTF-8 bytes, not UTF-16 code
|
|
161
|
+
units** — the 512-byte line cap, `TOPICLEN` (truncated on
|
|
162
|
+
character boundaries, never mid-multibyte), `NICKLEN` /
|
|
163
|
+
`CHANNELLEN`, and the `draft/multiline` batch budget, which is
|
|
164
|
+
now also **enforced incrementally as lines arrive** (an
|
|
165
|
+
over-budget batch aborts mid-stream with `FAIL`, not just at
|
|
166
|
+
close).
|
|
167
|
+
- **IRCv3 tag limits enforced in the parser**: at most **15 tags**
|
|
168
|
+
and an **8191-byte tag section** per message.
|
|
169
|
+
- **Multi-target commands capped.** Comma-split
|
|
170
|
+
`PRIVMSG`/`NOTICE`/`TAGMSG`/`PART` targets beyond
|
|
171
|
+
`MAX_TARGETS_PER_COMMAND` are dropped with `407 ERR_TOOMANYTARGETS`.
|
|
172
|
+
- **Long `353 NAMES` replies split** across multiple lines within
|
|
173
|
+
the 510-byte budget (previously one over-long line).
|
|
174
|
+
- **`CHATHISTORY` limits capped** at a configurable ceiling
|
|
175
|
+
(default 100) — `CHATHISTORY LATEST #chan * 999999999` no longer
|
|
176
|
+
asks for an unbounded replay.
|
|
177
|
+
- **Bare-CR smuggling rejected.** Any CR remaining after the
|
|
178
|
+
trailing CRLF strip is a parse error (`421`), and outbound
|
|
179
|
+
`RawLine.text` is sanitized of stray CR.
|
|
180
|
+
- **Channel names validated in one shared module** — NUL and
|
|
181
|
+
control characters are forbidden outright.
|
|
182
|
+
- **`USER` username/realname validated** (charset, byte length, no
|
|
183
|
+
control characters) before `001`.
|
|
184
|
+
- **`OPER` requires a registered connection** (`451
|
|
185
|
+
ERR_NOTREGISTERED` pre-registration).
|
|
186
|
+
- **`parseTags` builds a null-prototype map** — a `__proto__` tag
|
|
187
|
+
can no longer pollute the tags object (prototype pollution).
|
|
188
|
+
|
|
189
|
+
### Changed — Services hardening (`irc-core`)
|
|
190
|
+
|
|
191
|
+
- **ChanServ privilege guards.** `DEOP`/`KICK` cannot target the
|
|
192
|
+
founder from a non-founder, and roster-mutation commands check the
|
|
193
|
+
caller's access rank (`SOP`/`AOP`/`HOP`/`VOP`) before acting.
|
|
194
|
+
- **HostServ auto-approve denylist/allowlist.** When vhost
|
|
195
|
+
auto-approve is enabled, requests are checked against a
|
|
196
|
+
configurable denylist (and optional allowlist) instead of
|
|
197
|
+
approving everything.
|
|
198
|
+
|
|
199
|
+
### Added — CI / deploy guards (`ci-hardening`, deploy workflows)
|
|
200
|
+
|
|
201
|
+
- **Env-var documentation drift guard.** The
|
|
202
|
+
`consumed-env-vars` block in `apps/cf-worker/wrangler.toml` is
|
|
203
|
+
contract-checked against the cf-adapter config loader source on
|
|
204
|
+
every CI run: every consumed var must be documented (classified
|
|
205
|
+
`[vars]` vs `[secret]`), no stale names, and no `[secret]`-class
|
|
206
|
+
var may ship as a plaintext `[vars]` entry.
|
|
207
|
+
- **Placeholder-hostname deploy rejection.** The CF deploy workflow
|
|
208
|
+
refuses to ship while `SERVER_NAME` is still a repo placeholder
|
|
209
|
+
(`irc.example.com` / `irc.your-domain.invalid`).
|
|
210
|
+
|
|
211
|
+
### Changed — Documentation
|
|
212
|
+
|
|
213
|
+
- **`README.md` rewritten to current capabilities**: the abuse
|
|
214
|
+
controls & credential hardening surface (admission/rate table,
|
|
215
|
+
protocol budgets, lockouts, the SASL EXTERNAL triple gate), the
|
|
216
|
+
hardened TCP origins (PROXY v1 + origin TLS on CF, NLB assertions
|
|
217
|
+
on AWS), the new tools, accurate coverage state, and an updated
|
|
218
|
+
roadmap.
|
|
219
|
+
|
|
220
|
+
### ⚠️ Migration required
|
|
221
|
+
|
|
222
|
+
- **No persisted-DO-state schema change.** `PERSISTED_STATE_VERSION`
|
|
223
|
+
stays `1`; the D1/DynamoDB services tables are unchanged (the
|
|
224
|
+
canonical-DN rewrite happens lazily at compare time and is
|
|
225
|
+
backward-compatible). **Code rollback across this release is
|
|
226
|
+
safe.**
|
|
227
|
+
- **SASL `EXTERNAL` is now OFF by default.** If you rely on mTLS
|
|
228
|
+
EXTERNAL, set `EXTERNAL_ENABLED=true` (Workers `[vars]` / Lambda
|
|
229
|
+
env) **and** ensure edge mTLS (API Shield / APIGW client certs)
|
|
230
|
+
is configured — the flag alone advertises nothing. Accounts
|
|
231
|
+
should re-bind to the certificate's DER fingerprint
|
|
232
|
+
(`fp:<hex>`) where the platform surfaces it.
|
|
233
|
+
- **CF TCP origin: reconfigure Spectrum before deploying.** The
|
|
234
|
+
container now terminates TLS and **requires PROXY protocol v1**
|
|
235
|
+
on every flow. Update the Spectrum app to
|
|
236
|
+
`proxy_protocol = "v1"` + `tls_mode = "full"`, mount the origin
|
|
237
|
+
certificate readable by UID 1000, and keep origin ingress locked
|
|
238
|
+
to Spectrum's IP ranges. See `docs/Cloudflare-TCP-Deployment.md`.
|
|
239
|
+
- **AWS: NLB target must preserve flow headers.** The stream
|
|
240
|
+
handler now rejects flows missing `x-forwarded-for` /
|
|
241
|
+
`x-forwarded-port` (an NLB target-group attribute) — a
|
|
242
|
+
misconfigured LB now fails loudly instead of mis-attributing IPs.
|
|
243
|
+
- **Admission defaults are now ON.** `MAX_CONNECTIONS_PER_IP=10`,
|
|
244
|
+
per-user 5, connect-rate 5/60 s, and 50 frames / 5 s apply even
|
|
245
|
+
when unset. Deployments behind a shared NAT / proxy (offices,
|
|
246
|
+
bridges, health probes) should raise `MAX_CONNECTIONS_PER_IP` /
|
|
247
|
+
the rate budget to taste.
|
|
248
|
+
- **Oper credentials: migrate off plaintext.** `OPER_PASSWORD`
|
|
249
|
+
still works but is deprecated. Generate
|
|
250
|
+
`OPER_SALT`/`OPER_HASH` with `tools/hash-oper-cred.ts` and set
|
|
251
|
+
them as Workers secrets / Lambda env.
|
|
252
|
+
- **Client-visible behaviour changes.** (1) `AUTHENTICATE` frames
|
|
253
|
+
now count against flood control (a client that sent SASL at line
|
|
254
|
+
rate will be disconnected as an excess flood). (2) An inbound
|
|
255
|
+
frame with an embedded bare CR is now a parse error answered with
|
|
256
|
+
`421` (previously in legacy mode it could smuggle content).
|
|
257
|
+
(3) Multi-target commands beyond the cap get `407` for the
|
|
258
|
+
dropped targets. No compliant client is known to trip any of
|
|
259
|
+
these.
|
|
260
|
+
|
|
261
|
+
### Known limitations
|
|
262
|
+
|
|
263
|
+
- **Client compatibility sweep** (WeeChat / HexChat / IRCCloud /
|
|
264
|
+
TheLounge / matrix-IRC bridge) remains pending.
|
|
265
|
+
- **Web client e2e** (Playwright headless-browser vs a deployed
|
|
266
|
+
stack) remains pending; the AWS staging frontend e2e
|
|
267
|
+
(TICKET-169) is still open.
|
|
268
|
+
- Same transport limits as v0.10.0: the **CF TCP path requires
|
|
269
|
+
Cloudflare Spectrum (Enterprise tier)**; the **AWS TCP path** is
|
|
270
|
+
subject to Lambda stream-duration limits. WebSocket remains the
|
|
271
|
+
zero-extra-deps default.
|
|
272
|
+
- **Coverage hardening is near-complete.** `irc-core`,
|
|
273
|
+
`irc-server`, `in-memory-runtime`, `irc-test-support`,
|
|
274
|
+
`cf-worker`, `local-cli`, `aws-stack`, `load-test`,
|
|
275
|
+
`cf-tcp-container`, `tcp-ws-forwarder`, `ci-hardening`, and
|
|
276
|
+
`deploy-scripts` sit at 100% lines; `cf-adapter` (~99.7%) and
|
|
277
|
+
`aws-adapter` (~98%) clear the 90% gate with follow-ups open;
|
|
278
|
+
`web` is at 100% lines / ~98% branches.
|
|
279
|
+
|
|
280
|
+
### Security
|
|
281
|
+
|
|
282
|
+
This release is itself a security remediation; the headline items:
|
|
283
|
+
|
|
284
|
+
- **Admission + rate limiting at every edge** (above) — the audit's
|
|
285
|
+
"no throttling anywhere" theme: connection floods, per-IP
|
|
286
|
+
takeovers of the connection budget, and frame floods are now
|
|
287
|
+
bounded by default.
|
|
288
|
+
- **Plaintext oper credentials replaced** with scrypt hashes +
|
|
289
|
+
timing-safe verify (above).
|
|
290
|
+
- **Account-enumeration oracles closed**: timing-equalized
|
|
291
|
+
`verifyNick`, constant-time server-password compare (above).
|
|
292
|
+
- **Brute-force lockouts** on SASL / OPER / NickServ `IDENTIFY`
|
|
293
|
+
(above).
|
|
294
|
+
- **SASL `EXTERNAL` safe by default** (off unless the operator
|
|
295
|
+
opts in; fingerprint-based binding) and **bare-CR / prototype-
|
|
296
|
+
pollution / oversized-payload parser hardening** (above).
|
|
297
|
+
- **Credential redaction from logs** + 10% default sampling
|
|
298
|
+
(above), complementing v0.10.0's `DataTraceEnabled` hard-lock.
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
13
302
|
## [0.10.0] - 2026-08-13
|
|
14
303
|
|
|
15
304
|
The main change in this release is **AWS web client parity** — the
|