serverless-ircd 0.9.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-aws.yml +156 -32
- package/.github/workflows/deploy-cf-tcp.yml +35 -9
- package/.github/workflows/deploy-cf.yml +40 -14
- package/CHANGELOG.md +594 -0
- package/README.md +286 -60
- package/apps/aws-stack/README.md +3 -5
- package/apps/aws-stack/bin/aws.ts +118 -9
- package/apps/aws-stack/cdk.json +0 -3
- package/apps/aws-stack/package.json +3 -4
- package/apps/aws-stack/src/aws-stack.ts +398 -67
- package/apps/aws-stack/src/static-site.ts +323 -0
- package/apps/aws-stack/tests/smoke-helpers.test.ts +1 -1
- package/apps/aws-stack/tests/stack.test.ts +714 -105
- package/apps/aws-stack/tests/static-site.test.ts +491 -0
- package/apps/aws-stack/tests/synth-no-bundle.test.ts +0 -1
- package/apps/cf-tcp-container/Dockerfile +37 -5
- package/apps/cf-tcp-container/package.json +7 -3
- package/apps/cf-tcp-container/src/config-loader.ts +113 -2
- package/apps/cf-tcp-container/src/container-server.ts +267 -87
- 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 +18 -14
- package/apps/cf-worker/package.json +3 -4
- 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 +95 -77
- 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 +44 -19
- package/apps/local-cli/tests/line-scanner.test.ts +64 -0
- package/apps/local-cli/tests/tcp.test.ts +29 -0
- package/apps/web/landing/favicon.ico +0 -0
- package/apps/web/landing/index.html +1 -0
- package/apps/web/package.json +2 -2
- package/apps/web/scripts/build.mjs +66 -4
- package/apps/web/src/build-env.ts +125 -4
- package/apps/web/src/config-schema.ts +20 -6
- package/apps/web/static/{config.staging.json → config.prod-aws.json} +3 -2
- package/apps/web/tests/build-env.test.ts +210 -9
- package/apps/web/tests/build-smoke.test.ts +2 -2
- package/apps/web/tests/config-schema.test.ts +149 -25
- package/docs/AWS-Deployment.md +793 -118
- package/docs/AWS-TCP-Deployment.md +57 -47
- package/docs/Chat-History.md +55 -0
- package/docs/Cloudflare-Deployment-Guide.md +95 -114
- package/docs/Cloudflare-TCP-Deployment.md +160 -101
- package/docs/Release-Process.md +27 -23
- package/docs/SASL-EXTERNAL.md +175 -0
- package/docs/Services.md +69 -22
- package/docs/WebClientGuide.md +35 -26
- package/package.json +7 -10
- 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 +39 -16
- package/packages/aws-adapter/src/config-loader.ts +153 -8
- package/packages/aws-adapter/src/dynamo-services-store.ts +19 -0
- package/packages/aws-adapter/src/handlers/connect.ts +73 -1
- package/packages/aws-adapter/src/handlers/default.ts +279 -123
- package/packages/aws-adapter/src/handlers/index.ts +98 -25
- package/packages/aws-adapter/src/handlers/nlb-stream.ts +135 -14
- package/packages/aws-adapter/src/index.ts +5 -7
- package/packages/aws-adapter/src/ip-admission.ts +79 -0
- package/packages/aws-adapter/src/origin-allowlist.ts +94 -0
- package/packages/aws-adapter/src/serialize.ts +23 -0
- package/packages/aws-adapter/src/tables.ts +11 -12
- package/packages/aws-adapter/tests/admission.test.ts +60 -2
- package/packages/aws-adapter/tests/aws-harness.ts +23 -2
- package/packages/aws-adapter/tests/aws-runtime.test.ts +64 -0
- package/packages/aws-adapter/tests/config-loader.test.ts +217 -0
- package/packages/aws-adapter/tests/connect.test.ts +323 -3
- package/packages/aws-adapter/tests/default-frame-limit.test.ts +231 -0
- package/packages/aws-adapter/tests/default-occ.test.ts +226 -0
- package/packages/aws-adapter/tests/dynamo-services-store-unit.test.ts +134 -1
- package/packages/aws-adapter/tests/handlers.test.ts +174 -12
- package/packages/aws-adapter/tests/migrate-accounts-to-services.test.ts +164 -0
- package/packages/aws-adapter/tests/nlb-secure.test.ts +362 -0
- package/packages/aws-adapter/tests/nlb-stream.test.ts +628 -9
- package/packages/aws-adapter/tests/origin-allowlist.test.ts +110 -0
- package/packages/aws-adapter/tests/ping-checker.test.ts +0 -1
- package/packages/aws-adapter/tests/stats.test.ts +0 -3
- package/packages/aws-adapter/tests/sweeper.test.ts +0 -1
- package/packages/aws-adapter/tests/tables.test.ts +1 -8
- package/packages/aws-adapter/tests/transactions.test.ts +0 -1
- package/packages/cf-adapter/package.json +1 -5
- package/packages/cf-adapter/src/cf-runtime.ts +100 -10
- package/packages/cf-adapter/src/channel-do.ts +13 -3
- package/packages/cf-adapter/src/config-loader.ts +133 -8
- package/packages/cf-adapter/src/connection-do.ts +406 -116
- package/packages/cf-adapter/src/counter-do.ts +142 -0
- package/packages/cf-adapter/src/d1-services-store.ts +105 -26
- package/packages/cf-adapter/src/env.ts +99 -10
- package/packages/cf-adapter/src/index.ts +17 -7
- package/packages/cf-adapter/src/rate-limit-do.ts +87 -0
- package/packages/cf-adapter/tests/cf-runtime.test.ts +205 -16
- package/packages/cf-adapter/tests/channel-do.test.ts +118 -1
- 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-coverage.test.ts +460 -0
- package/packages/cf-adapter/tests/connection-do-frame-limit.test.ts +177 -0
- package/packages/cf-adapter/tests/connection-do-pure.test.ts +164 -54
- package/packages/cf-adapter/tests/connection-do-sasl-d1.test.ts +62 -38
- 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 +245 -3
- package/packages/cf-adapter/tests/rate-limit-do.test.ts +160 -0
- package/packages/cf-adapter/tests/serialize.test.ts +25 -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/account-migration.ts +140 -0
- 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/account-auth.ts +16 -19
- 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/memoserv.ts +1 -1
- 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 +161 -11
- 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 +148 -4
- package/packages/irc-core/src/commands/sasl.ts +154 -46
- 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 +424 -25
- package/packages/irc-core/src/credential-hashing.ts +11 -54
- 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 +9 -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 +529 -190
- 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 +266 -23
- package/packages/irc-core/src/ws-framing.ts +5 -4
- package/packages/irc-core/tests/account-migration.test.ts +133 -0
- 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/markread.test.ts +54 -0
- package/packages/irc-core/tests/commands/memoserv.test.ts +19 -0
- package/packages/irc-core/tests/commands/names.test.ts +193 -0
- package/packages/irc-core/tests/commands/nickserv.test.ts +419 -3
- package/packages/irc-core/tests/commands/oper.test.ts +574 -1
- package/packages/irc-core/tests/commands/privmsg.test.ts +16 -0
- package/packages/irc-core/tests/commands/registration.test.ts +602 -133
- package/packages/irc-core/tests/commands/sasl.test.ts +742 -172
- package/packages/irc-core/tests/commands/topic.test.ts +137 -2
- package/packages/irc-core/tests/commands/unified-account.test.ts +104 -84
- package/packages/irc-core/tests/config.test.ts +534 -2
- package/packages/irc-core/tests/credential-hashing.test.ts +0 -78
- 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/message-store.test.ts +5 -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 +212 -12
- package/packages/irc-core/tests/ports.test.ts +170 -7
- package/packages/irc-core/tests/services-store.test.ts +567 -1
- package/packages/irc-core/tests/ws-framing.test.ts +45 -0
- package/packages/irc-core/vitest.config.ts +6 -1
- package/packages/irc-server/package.json +1 -1
- package/packages/irc-server/src/actor.ts +123 -22
- 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 +563 -54
- 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/pnpm-workspace.yaml +1 -0
- package/scripts/__tests__/deploy-web-aws.test.ts +491 -0
- package/scripts/deploy-web-aws.mjs +290 -0
- package/scripts/package.json +23 -0
- package/scripts/tsconfig.test.json +12 -0
- package/scripts/vitest.config.ts +19 -0
- 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 +19 -0
- package/tools/ci-hardening/src/validate.ts +57 -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-aws-oidc.test.ts +96 -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/tests/validate.test.ts +42 -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/migrate-accounts-to-services.ts +270 -0
- package/tools/package.json +2 -1
- package/tools/seed-aws-accounts.ts +35 -10
- package/tools/seed-cf-accounts.ts +42 -9
- package/tools/tcp-ws-forwarder/package.json +1 -1
- package/packages/aws-adapter/src/account-store.ts +0 -121
- package/packages/aws-adapter/src/dynamo-account-store.ts +0 -95
- package/packages/aws-adapter/tests/account-store-dynamo.test.ts +0 -223
- package/packages/aws-adapter/tests/account-store.test.ts +0 -276
- package/packages/cf-adapter/src/d1-account-store.ts +0 -198
- package/packages/cf-adapter/tests/d1-account-store.test.ts +0 -274
- package/packages/irc-core/tests/account-store.test.ts +0 -131
|
@@ -2,13 +2,18 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* CF TCP container origin entry point.
|
|
4
4
|
*
|
|
5
|
-
* Boots the IRC core as a
|
|
6
|
-
* Spectrum
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* Boots the IRC core as a TLS-terminating origin behind Cloudflare
|
|
6
|
+
* Spectrum (`tls_mode = "full"`): the process presents its own
|
|
7
|
+
* certificate on :6697 (TLS_CERT_PATH / TLS_KEY_PATH) and Spectrum
|
|
8
|
+
* re-encrypts edge→origin, so no plaintext IRC crosses the network.
|
|
9
|
+
* A plaintext listener is available only for localhost debugging
|
|
10
|
+
* (DEBUG_PLAINTEXT=localhost, bound to 127.0.0.1). Configuration comes
|
|
11
|
+
* from environment variables (set by the Cloudflare Container runtime,
|
|
12
|
+
* Docker, or Kubernetes).
|
|
9
13
|
*
|
|
10
14
|
* Usage:
|
|
11
|
-
*
|
|
15
|
+
* TLS_CERT_PATH=/certs/tls.pem TLS_KEY_PATH=/certs/tls.key \
|
|
16
|
+
* SERVER_NAME=irc.example.com node dist/main.js
|
|
12
17
|
*
|
|
13
18
|
* The process runs until SIGINT/SIGTERM. On shutdown it writes a final
|
|
14
19
|
* persistence snapshot (when PERSISTENCE_PATH is set) so channel state
|
|
@@ -21,9 +26,17 @@ import { startContainerServer } from './container-server.js';
|
|
|
21
26
|
async function main(): Promise<void> {
|
|
22
27
|
const cfg = loadContainerConfig(process.env as Record<string, string | undefined>);
|
|
23
28
|
|
|
29
|
+
if (!cfg.plaintextDebug && cfg.tls === undefined) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
'no listener configured: set TLS_CERT_PATH + TLS_KEY_PATH to serve TLS on :6697, ' +
|
|
32
|
+
'or DEBUG_PLAINTEXT=localhost for a localhost-only plaintext debug listener',
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
24
36
|
const server = await startContainerServer({
|
|
25
|
-
port: cfg.tcpPort,
|
|
26
37
|
host: cfg.tcpHost,
|
|
38
|
+
...(cfg.plaintextDebug ? { port: cfg.tcpPort } : {}),
|
|
39
|
+
...(cfg.tls !== undefined ? { tls: cfg.tls } : {}),
|
|
27
40
|
serverName: cfg.server.serverName,
|
|
28
41
|
networkName: cfg.server.networkName,
|
|
29
42
|
motdLines: cfg.server.motdLines,
|
|
@@ -47,7 +60,9 @@ async function main(): Promise<void> {
|
|
|
47
60
|
level: 'info',
|
|
48
61
|
msg: 'cf-tcp-container listening',
|
|
49
62
|
host: server.host,
|
|
50
|
-
port: server.port,
|
|
63
|
+
port: server.port ?? null,
|
|
64
|
+
tlsPort: server.tlsPort ?? null,
|
|
65
|
+
plaintextDebug: cfg.plaintextDebug,
|
|
51
66
|
persistence: cfg.persistencePath ?? null,
|
|
52
67
|
serverName: cfg.server.serverName,
|
|
53
68
|
})}\n`,
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PROXY protocol v1 header parsing.
|
|
3
|
+
*
|
|
4
|
+
* Cloudflare Spectrum fronts this origin with `proxy_protocol = "v1"`:
|
|
5
|
+
* every forwarded TCP flow begins with the ASCII line
|
|
6
|
+
*
|
|
7
|
+
* PROXY TCP4 <src_ip> <dst_ip> <src_port> <dst_port>\r\n
|
|
8
|
+
* PROXY TCP6 <src_ip> <dst_ip> <src_port> <dst_port>\r\n
|
|
9
|
+
*
|
|
10
|
+
* (spec: the header is at most 107 bytes including the trailing CRLF).
|
|
11
|
+
* The source IP recovered here is the real client address — the TCP
|
|
12
|
+
* peer address seen by this origin is always a Spectrum edge IP.
|
|
13
|
+
*
|
|
14
|
+
* Dependency decision: hand-rolled parser (no npm `proxy-protocol`
|
|
15
|
+
* dependency). The v1 header is one fixed-format, ASCII-only line; the
|
|
16
|
+
* ~40 lines below fully validate it while avoiding the supply-chain
|
|
17
|
+
* surface of a third-party package on the connection hot path. (The
|
|
18
|
+
* decision is also recorded in this package's package.json.)
|
|
19
|
+
*
|
|
20
|
+
* The header may arrive split across TCP chunks, so
|
|
21
|
+
* {@link consumeProxyV1Header} is incremental: feed it the bytes
|
|
22
|
+
* buffered so far and it reports `incomplete` until the trailing CRLF
|
|
23
|
+
* is available, `invalid` for anything that can no longer be a legal
|
|
24
|
+
* header (wrong signature, over-length, unknown family, malformed
|
|
25
|
+
* address/port), or `complete` with the recovered source IP plus any
|
|
26
|
+
* bytes that followed the header.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { isIP } from 'node:net';
|
|
30
|
+
|
|
31
|
+
/** Max PROXY v1 header length in bytes, including the trailing CRLF. */
|
|
32
|
+
export const PROXY_V1_MAX_HEADER_BYTES = 107;
|
|
33
|
+
|
|
34
|
+
const SIGNATURE = 'PROXY ';
|
|
35
|
+
const CRLF = '\r\n';
|
|
36
|
+
|
|
37
|
+
/** Outcome of {@link consumeProxyV1Header}. */
|
|
38
|
+
export type ProxyV1HeaderResult =
|
|
39
|
+
| { readonly status: 'complete'; readonly sourceIp: string; readonly remainder: Buffer }
|
|
40
|
+
| { readonly status: 'incomplete' }
|
|
41
|
+
| { readonly status: 'invalid' };
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Consumes a PROXY protocol v1 header from `buffer` (the bytes received
|
|
45
|
+
* on the connection so far). Pure function: the caller keeps
|
|
46
|
+
* concatenating chunks and re-invoking until the result is no longer
|
|
47
|
+
* `incomplete`.
|
|
48
|
+
*/
|
|
49
|
+
export function consumeProxyV1Header(buffer: Buffer): ProxyV1HeaderResult {
|
|
50
|
+
const crlf = buffer.indexOf(CRLF);
|
|
51
|
+
if (crlf === -1) {
|
|
52
|
+
// No terminator yet. Reject as soon as the bytes can no longer be a
|
|
53
|
+
// header prefix: over the 107-byte budget, or not spelling "PROXY ".
|
|
54
|
+
if (buffer.length > PROXY_V1_MAX_HEADER_BYTES) {
|
|
55
|
+
return { status: 'invalid' };
|
|
56
|
+
}
|
|
57
|
+
const prefix = buffer.subarray(0, SIGNATURE.length).toString('ascii');
|
|
58
|
+
if (!SIGNATURE.startsWith(prefix)) {
|
|
59
|
+
return { status: 'invalid' };
|
|
60
|
+
}
|
|
61
|
+
return { status: 'incomplete' };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const headerEnd = crlf + CRLF.length;
|
|
65
|
+
if (headerEnd > PROXY_V1_MAX_HEADER_BYTES) {
|
|
66
|
+
return { status: 'invalid' };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// One slot per expected token plus a trailing slot that must stay
|
|
70
|
+
// empty: fewer than 6 tokens surfaces as `undefined`, more than 6
|
|
71
|
+
// populates `extra`.
|
|
72
|
+
const [signature, family, srcIp, dstIp, srcPort, dstPort, extra] = buffer
|
|
73
|
+
.subarray(0, crlf)
|
|
74
|
+
.toString('ascii')
|
|
75
|
+
.split(' ');
|
|
76
|
+
if (
|
|
77
|
+
family === undefined ||
|
|
78
|
+
srcIp === undefined ||
|
|
79
|
+
dstIp === undefined ||
|
|
80
|
+
srcPort === undefined ||
|
|
81
|
+
dstPort === undefined ||
|
|
82
|
+
extra !== undefined ||
|
|
83
|
+
signature !== 'PROXY'
|
|
84
|
+
) {
|
|
85
|
+
return { status: 'invalid' };
|
|
86
|
+
}
|
|
87
|
+
const expectedFamily = family === 'TCP4' ? 4 : family === 'TCP6' ? 6 : 0;
|
|
88
|
+
if (expectedFamily === 0) {
|
|
89
|
+
// Spectrum always announces TCP4/TCP6; UNKNOWN (and anything else)
|
|
90
|
+
// is rejected so a header cannot be used to hide the source address.
|
|
91
|
+
return { status: 'invalid' };
|
|
92
|
+
}
|
|
93
|
+
if (isIP(srcIp) !== expectedFamily || isIP(dstIp) !== expectedFamily) {
|
|
94
|
+
return { status: 'invalid' };
|
|
95
|
+
}
|
|
96
|
+
if (!isValidPort(srcPort) || !isValidPort(dstPort)) {
|
|
97
|
+
return { status: 'invalid' };
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
status: 'complete',
|
|
101
|
+
sourceIp: srcIp,
|
|
102
|
+
remainder: buffer.subarray(headerEnd),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function isValidPort(token: string): boolean {
|
|
107
|
+
if (!/^\d{1,5}$/u.test(token)) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
const value = Number.parseInt(token, 10);
|
|
111
|
+
return value >= 0 && value <= 65_535;
|
|
112
|
+
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
###############################################################################
|
|
2
2
|
# Cloudflare Spectrum config for the ServerlessIRCd TCP+TLS transport.
|
|
3
3
|
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
4
|
+
# TLS is end-to-end: clients terminate TLS with Spectrum on :6697 and
|
|
5
|
+
# Spectrum re-encrypts to the origin, which runs its own TLS listener on
|
|
6
|
+
# :6697 (see apps/cf-tcp-container/src/container-server.ts). No plaintext
|
|
7
|
+
# IRC bytes ever cross the origin network, so a direct-reach attacker can
|
|
8
|
+
# neither read PASS/AUTHENTICATE/PRIVMSG off the wire nor spoof them.
|
|
7
9
|
#
|
|
8
10
|
# Usage:
|
|
9
11
|
# terraform init
|
|
@@ -12,10 +14,26 @@
|
|
|
12
14
|
# -var origin_address=container-origin.example.com \
|
|
13
15
|
# -var hostname=irc.example.com
|
|
14
16
|
#
|
|
15
|
-
# The `tls = "
|
|
16
|
-
#
|
|
17
|
-
# TCP
|
|
18
|
-
#
|
|
17
|
+
# The `tls = "full"` default means the origin must present a certificate
|
|
18
|
+
# the TLS listener accepts (Let's Encrypt, BYO, or a Cloudflare Origin
|
|
19
|
+
# Cert — see docs/Cloudflare-TCP-Deployment.md). For strict origin-cert
|
|
20
|
+
# validation at the edge, set `tls_mode = "strict"`.
|
|
21
|
+
#
|
|
22
|
+
# Origin ingress lockdown (operator-side step): restrict ingress to the
|
|
23
|
+
# origin's :6697 to Cloudflare's published IP ranges so the origin is only
|
|
24
|
+
# reachable through Spectrum. On platforms with a firewall / security-group
|
|
25
|
+
# primitive, source the ranges from the current Cloudflare IP list, e.g.:
|
|
26
|
+
#
|
|
27
|
+
# data "cloudflare_lists" "all" {}
|
|
28
|
+
#
|
|
29
|
+
# resource "cloudflare_list" "cf_ips" {
|
|
30
|
+
# account_id = var.cloudflare_account_id
|
|
31
|
+
# name = "cloudflare-ips"
|
|
32
|
+
# kind = "IP"
|
|
33
|
+
# }
|
|
34
|
+
#
|
|
35
|
+
# (Maintained list source: https://www.cloudflare.com/ips/ — keep the
|
|
36
|
+
# firewall entries in sync when Cloudflare publishes range changes.)
|
|
19
37
|
###############################################################################
|
|
20
38
|
|
|
21
39
|
variable "zone_id" {
|
|
@@ -30,8 +48,8 @@ variable "origin_address" {
|
|
|
30
48
|
|
|
31
49
|
variable "origin_port" {
|
|
32
50
|
type = number
|
|
33
|
-
default =
|
|
34
|
-
description = "Port the
|
|
51
|
+
default = 6697
|
|
52
|
+
description = "Port the origin's TLS listener serves (Spectrum re-encrypts to it)."
|
|
35
53
|
}
|
|
36
54
|
|
|
37
55
|
variable "hostname" {
|
|
@@ -48,8 +66,8 @@ variable "spectrum_port" {
|
|
|
48
66
|
|
|
49
67
|
variable "tls_mode" {
|
|
50
68
|
type = string
|
|
51
|
-
default = "
|
|
52
|
-
description = "Spectrum TLS mode:
|
|
69
|
+
default = "full"
|
|
70
|
+
description = "Spectrum TLS mode: full (TLS end-to-end, the secure default) or strict (edge validates the origin cert)."
|
|
53
71
|
|
|
54
72
|
validation {
|
|
55
73
|
condition = contains(["flexible", "full", "strict"], var.tls_mode)
|
|
@@ -75,6 +93,17 @@ resource "cloudflare_spectrum_application" "irc_tls" {
|
|
|
75
93
|
ip_firewall = true
|
|
76
94
|
argo_smart_routing = true
|
|
77
95
|
|
|
96
|
+
# NOTE on TLS version pinning (1.2 / 1.3 only): Spectrum TCP
|
|
97
|
+
# applications CANNOT pin per-app TLS versions — the Cloudflare API
|
|
98
|
+
# and the terraform provider expose no `tls_versions` field for them
|
|
99
|
+
# (setting one fails `terraform plan` with "Unsupported argument"),
|
|
100
|
+
# and Cloudflare's fixed defaults apply at the edge: SSLv3 and TLS 1.0
|
|
101
|
+
# are never negotiable, TLS 1.1 is the floor. The explicit TLS 1.2/1.3
|
|
102
|
+
# pin for this deployment is therefore enforced on the origin leg in
|
|
103
|
+
# `apps/cf-tcp-container/src/container-server.ts`: the origin TLS
|
|
104
|
+
# listener terminates the re-encrypted Spectrum→origin connection
|
|
105
|
+
# with `minVersion: 'TLSv1.2'` / `maxVersion: 'TLSv1.3'`.
|
|
106
|
+
|
|
78
107
|
# PROXY protocol v1 forwards the real client IP to the origin so the
|
|
79
108
|
# container's admission gate (per-IP limits) sees the actual source.
|
|
80
109
|
proxy_protocol = "v1"
|
|
@@ -161,6 +161,29 @@ describe('config-loader — container-specific knobs', () => {
|
|
|
161
161
|
expect(cfg.server.operCreds).toEqual([{ user: 'admin', password: 'secret' }]);
|
|
162
162
|
});
|
|
163
163
|
|
|
164
|
+
it('parses OPER_USER + OPER_SALT + OPER_HASH into a hashed operCred', () => {
|
|
165
|
+
const cfg = loadContainerConfig({
|
|
166
|
+
SERVER_NAME: 'irc.test',
|
|
167
|
+
NETWORK_NAME: 'TestNet',
|
|
168
|
+
OPER_USER: 'admin',
|
|
169
|
+
OPER_SALT: 'c2FsdA==',
|
|
170
|
+
OPER_HASH: 'aGFzaA==',
|
|
171
|
+
});
|
|
172
|
+
expect(cfg.server.operCreds).toEqual([{ user: 'admin', salt: 'c2FsdA==', hash: 'aGFzaA==' }]);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('prefers the hashed operCred form when both plaintext and hashed env vars are set', () => {
|
|
176
|
+
const cfg = loadContainerConfig({
|
|
177
|
+
SERVER_NAME: 'irc.test',
|
|
178
|
+
NETWORK_NAME: 'TestNet',
|
|
179
|
+
OPER_USER: 'admin',
|
|
180
|
+
OPER_PASSWORD: 'legacy',
|
|
181
|
+
OPER_SALT: 'c2FsdA==',
|
|
182
|
+
OPER_HASH: 'aGFzaA==',
|
|
183
|
+
});
|
|
184
|
+
expect(cfg.server.operCreds).toEqual([{ user: 'admin', salt: 'c2FsdA==', hash: 'aGFzaA==' }]);
|
|
185
|
+
});
|
|
186
|
+
|
|
164
187
|
it('exercises the OPER_PASSWORD ?? "" default then rejects partial OPER_USER-only creds', () => {
|
|
165
188
|
// The `?? ''` default for the missing OPER_PASSWORD is exercised, but
|
|
166
189
|
// parseServerConfig then rejects the empty password — partial oper
|
|
@@ -265,3 +288,150 @@ describe('config-loader — container-specific knobs', () => {
|
|
|
265
288
|
expect(cfg.server.serverName).toBe('irc.test');
|
|
266
289
|
});
|
|
267
290
|
});
|
|
291
|
+
|
|
292
|
+
describe('config-loader — TLS termination at the origin', () => {
|
|
293
|
+
it('tls is undefined when no cert env is set (non-prod)', () => {
|
|
294
|
+
const cfg = loadContainerConfig({ SERVER_NAME: 'irc.test', NETWORK_NAME: 'TestNet' });
|
|
295
|
+
expect(cfg.tls).toBeUndefined();
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('reads TLS_CERT_PATH + TLS_KEY_PATH into a tls config with default port 6697', () => {
|
|
299
|
+
const cfg = loadContainerConfig({
|
|
300
|
+
SERVER_NAME: 'irc.test',
|
|
301
|
+
NETWORK_NAME: 'TestNet',
|
|
302
|
+
TLS_CERT_PATH: '/data/certs/tls.pem',
|
|
303
|
+
TLS_KEY_PATH: '/data/certs/tls.key',
|
|
304
|
+
});
|
|
305
|
+
expect(cfg.tls).toEqual({
|
|
306
|
+
certPath: '/data/certs/tls.pem',
|
|
307
|
+
keyPath: '/data/certs/tls.key',
|
|
308
|
+
port: 6697,
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('reads TLS_PORT override', () => {
|
|
313
|
+
const cfg = loadContainerConfig({
|
|
314
|
+
SERVER_NAME: 'irc.test',
|
|
315
|
+
NETWORK_NAME: 'TestNet',
|
|
316
|
+
TLS_CERT_PATH: '/certs/tls.pem',
|
|
317
|
+
TLS_KEY_PATH: '/certs/tls.key',
|
|
318
|
+
TLS_PORT: '7000',
|
|
319
|
+
});
|
|
320
|
+
expect(cfg.tls?.port).toBe(7000);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('defaults TLS_PORT when TLS_PORT is empty or invalid', () => {
|
|
324
|
+
for (const bad of ['', 'not-a-number']) {
|
|
325
|
+
const cfg = loadContainerConfig({
|
|
326
|
+
SERVER_NAME: 'irc.test',
|
|
327
|
+
NETWORK_NAME: 'TestNet',
|
|
328
|
+
TLS_CERT_PATH: '/certs/tls.pem',
|
|
329
|
+
TLS_KEY_PATH: '/certs/tls.key',
|
|
330
|
+
TLS_PORT: bad,
|
|
331
|
+
});
|
|
332
|
+
expect(cfg.tls?.port).toBe(6697);
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it('throws naming TLS_KEY_PATH when only TLS_CERT_PATH is set', () => {
|
|
337
|
+
expect(() =>
|
|
338
|
+
loadContainerConfig({
|
|
339
|
+
SERVER_NAME: 'irc.test',
|
|
340
|
+
NETWORK_NAME: 'TestNet',
|
|
341
|
+
TLS_CERT_PATH: '/certs/tls.pem',
|
|
342
|
+
}),
|
|
343
|
+
).toThrowError(/TLS_KEY_PATH/u);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('throws naming TLS_CERT_PATH when only TLS_KEY_PATH is set', () => {
|
|
347
|
+
expect(() =>
|
|
348
|
+
loadContainerConfig({
|
|
349
|
+
SERVER_NAME: 'irc.test',
|
|
350
|
+
NETWORK_NAME: 'TestNet',
|
|
351
|
+
TLS_KEY_PATH: '/certs/tls.key',
|
|
352
|
+
}),
|
|
353
|
+
).toThrowError(/TLS_CERT_PATH/u);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it('refuses to start in prod-classified mode without TLS_CERT_PATH', () => {
|
|
357
|
+
expect(() =>
|
|
358
|
+
loadContainerConfig({
|
|
359
|
+
SERVER_NAME: 'irc.test',
|
|
360
|
+
NETWORK_NAME: 'TestNet',
|
|
361
|
+
NODE_ENV: 'production',
|
|
362
|
+
}),
|
|
363
|
+
).toThrowError(/TLS_CERT_PATH/u);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it('refuses DEBUG_PLAINTEXT in prod-classified mode even with TLS configured', () => {
|
|
367
|
+
expect(() =>
|
|
368
|
+
loadContainerConfig({
|
|
369
|
+
SERVER_NAME: 'irc.test',
|
|
370
|
+
NETWORK_NAME: 'TestNet',
|
|
371
|
+
NODE_ENV: 'production',
|
|
372
|
+
TLS_CERT_PATH: '/certs/tls.pem',
|
|
373
|
+
TLS_KEY_PATH: '/certs/tls.key',
|
|
374
|
+
DEBUG_PLAINTEXT: 'localhost',
|
|
375
|
+
}),
|
|
376
|
+
).toThrowError(/DEBUG_PLAINTEXT/u);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it('the prod no-TLS guard fires before the DEBUG_PLAINTEXT guard', () => {
|
|
380
|
+
expect(() =>
|
|
381
|
+
loadContainerConfig({
|
|
382
|
+
SERVER_NAME: 'irc.test',
|
|
383
|
+
NETWORK_NAME: 'TestNet',
|
|
384
|
+
NODE_ENV: 'production',
|
|
385
|
+
DEBUG_PLAINTEXT: 'localhost',
|
|
386
|
+
}),
|
|
387
|
+
).toThrowError(/TLS_CERT_PATH/u);
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
describe('config-loader — plaintext debug listener gate', () => {
|
|
392
|
+
it('plaintextDebug is false when DEBUG_PLAINTEXT is unset', () => {
|
|
393
|
+
const cfg = loadContainerConfig({ SERVER_NAME: 'irc.test', NETWORK_NAME: 'TestNet' });
|
|
394
|
+
expect(cfg.plaintextDebug).toBe(false);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it('enables plaintextDebug for the exact flag value DEBUG_PLAINTEXT=localhost', () => {
|
|
398
|
+
const cfg = loadContainerConfig({
|
|
399
|
+
SERVER_NAME: 'irc.test',
|
|
400
|
+
NETWORK_NAME: 'TestNet',
|
|
401
|
+
DEBUG_PLAINTEXT: 'localhost',
|
|
402
|
+
});
|
|
403
|
+
expect(cfg.plaintextDebug).toBe(true);
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it('forces the plaintext bind host to 127.0.0.1 when plaintextDebug is enabled', () => {
|
|
407
|
+
const cfg = loadContainerConfig({
|
|
408
|
+
SERVER_NAME: 'irc.test',
|
|
409
|
+
NETWORK_NAME: 'TestNet',
|
|
410
|
+
DEBUG_PLAINTEXT: 'localhost',
|
|
411
|
+
TCP_HOST: '0.0.0.0',
|
|
412
|
+
});
|
|
413
|
+
expect(cfg.plaintextDebug).toBe(true);
|
|
414
|
+
expect(cfg.tcpHost).toBe('127.0.0.1');
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it('rejects DEBUG_PLAINTEXT values other than localhost', () => {
|
|
418
|
+
expect(() =>
|
|
419
|
+
loadContainerConfig({
|
|
420
|
+
SERVER_NAME: 'irc.test',
|
|
421
|
+
NETWORK_NAME: 'TestNet',
|
|
422
|
+
DEBUG_PLAINTEXT: '0.0.0.0',
|
|
423
|
+
}),
|
|
424
|
+
).toThrowError(/localhost/u);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it('keeps the normal TCP_HOST default when plaintextDebug is disabled', () => {
|
|
428
|
+
const cfg = loadContainerConfig({
|
|
429
|
+
SERVER_NAME: 'irc.test',
|
|
430
|
+
NETWORK_NAME: 'TestNet',
|
|
431
|
+
TLS_CERT_PATH: '/certs/tls.pem',
|
|
432
|
+
TLS_KEY_PATH: '/certs/tls.key',
|
|
433
|
+
});
|
|
434
|
+
expect(cfg.plaintextDebug).toBe(false);
|
|
435
|
+
expect(cfg.tcpHost).toBe('0.0.0.0');
|
|
436
|
+
});
|
|
437
|
+
});
|