moon:// / moons:// connection URI schemeΒΆ
Status: SPEC β accepted design, doc-only (v0.6.1 task H-7). No Rust implementation
ships with this document. The shared parser (src/uri.rs), server-side --announce-url
plumbing, and tests/uri_scheme.rs conformance matrix are v0.7.0 Workstream R6
(see ROADMAP.md Β§8.2 and
Β§8.5).
This document is the source of truth that R6 must implement against and that
tests/uri_scheme.rs must be written to conform to.
Until R6 ships, moon:// / moons:// strings are not understood by any Moon binary β
moon-cli -u, client libraries, REPLICAOF, and CLUSTER MEET all still take
redis:// / rediss:// or bare host port today. See
Conformance for the exact behavior that must
exist before this scheme is considered "live."
MotivationΒΆ
Moon is Redis-wire-compatible, so redis:// / rediss:// connection strings work today
and must keep working β this spec changes nothing about that. But Moon is a
multi-model engine with first-class multi-tenancy (workspaces),
TLS 1.3, and (from v0.7.0) multi-shard replication. It deserves a native, self-branding
URL scheme the way Redis has its own β moon:// / moons:// are that scheme: a strict
superset of the Redis URI understood by clients, replication, cluster redirects, and
--announce-url. moons:// is the TLS variant, exactly like rediss:// is to redis://
and https:// is to http://.
Backward compatibility (non-negotiable). The scheme is a client-side transport +
routing convention only β zero wire-protocol change. redis:// / rediss:// remain
fully accepted and semantically identical for every overlapping field. Any client that
already allows a scheme override keeps working unmodified; moon(s):// is additive.
Grammar (ABNF)ΒΆ
moon-uri = scheme "://" [ userinfo "@" ] host [ ":" port ] [ "/" db-index ] [ "?" query ]
scheme = "moon" / "moons" ; "moons" = TLS 1.3 transport (rustls, aws-lc-rs)
userinfo = [ username ] [ ":" password ] ; maps to AUTH [user] pass
username = *( unreserved / pct-encoded / sub-delims )
password = *( unreserved / pct-encoded / sub-delims / ":" )
host = IP-literal / IPv4address / reg-name / unix-path-encoded
; IP-literal, IPv4address, reg-name per RFC 3986 Β§3.2.2
unix-path-encoded = "unix" ; reserved reg-name value β see "Unix sockets" below
port = 1*DIGIT
; moon: default 6379 if omitted (matches --port default)
; moons: NO implicit default β port MUST be given explicitly and
; MUST equal the server's configured --tls-port. There is no
; well-known "TLS port" the way 443 is to 80; guessing one
; would silently connect to the wrong listener or hang.
db-index = 1*DIGIT ; SELECT <db-index> on connect
query = param *( "&" param )
param = key "=" value
key = 1*( ALPHA / DIGIT / "_" )
value = *( unreserved / pct-encoded / sub-delims )
unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
pct-encoded = "%" HEXDIG HEXDIG
sub-delims = "!" / "$" / "'" / "(" / ")" / "*" / "+" / "," / ";"
Notes:
schemeis case-insensitive at parse time (Moon://,MOONS://are accepted) but Moon tooling always emits lowercase.hostfollows RFC 3986 host productions unchanged β IPv6 literals use bracket form (moon://[::1]:6379).- Percent-decoding of
userinfoandvaluehappens after field splitting, so a literal@,:,/,&, or=inside a username/password/value MUST be percent-encoded. - Unrecognized query keys are a parse error, not silently ignored β see Design-for-failure.
- Unix sockets are a reserved future extension (
moon://unix/path/to/socket, mirroring how some Redis clients overloadhost=unix). Not part of this spec's normative grammar beyond reserving the token; Moon has no Unix-socket listener today. If/when one ships, it gets its own ROADMAP entry and an addendum here β do not implement against this paragraph alone.
redis(s):// parity tableΒΆ
Every field below behaves identically whether the scheme is redis(s) or moon(s),
except where the "Moon-native" rows call out an extension. This table is normative for
the R6 parser: any divergence from it is a bug.
| Concern | redis(s):// behavior |
moon(s):// behavior |
|---|---|---|
| Transport | redis = plaintext, rediss = TLS |
moon = plaintext, moons = TLS 1.3 (rustls + aws-lc-rs) β identical selection rule |
| Default port | redis:// β 6379 |
moon:// β 6379; moons:// β no default, port is mandatory and must equal --tls-port |
| Auth | userinfo β AUTH [user] pass |
same |
| DB select | /N β SELECT N |
same |
| TLS peer verification | ?ssl_cert_reqs= |
same key accepted (alias, see below) |
| TLS CA bundle | ?ssl_ca_certs= |
same key accepted (alias) |
| Socket read/write timeout | ?socket_timeout= |
same |
| Connect timeout | ?socket_connect_timeout= |
same |
| Workspace selection | (none β client issues WS AUTH <ws_id> after connecting) |
?workspace=<id-or-name> selects the Moon workspace before the first application command β multi-tenancy is a shipped guarantee, not bolted on post-connect |
| Server self-announce | (n/a) | server emits moon(s):// URIs in INFO replication (master_announce_url), CLUSTER SHARDS/MOVED/ASK redirects, and replica handshake metadata |
Query-parameter referenceΒΆ
Parity parameters (shared with redis(s)://)ΒΆ
| Parameter | Type | Default | Example | Semantics |
|---|---|---|---|---|
ssl_cert_reqs |
enum none | optional | required |
required on moons/rediss; n/a on plaintext |
?ssl_cert_reqs=required |
Peer certificate verification mode. none disables verification (dev-only; client MUST warn). Server-side mTLS is independently controlled by --tls-ca-cert-file; this parameter is about the client's verification of the server cert. |
ssl_ca_certs |
path (string, percent-encoded) | (system trust store) | ?ssl_ca_certs=%2Fetc%2Fmoon%2Fca.pem |
Path to a PEM CA bundle the client uses to verify the server certificate. |
ssl_certfile |
path | (none) | ?ssl_certfile=%2Fetc%2Fmoon%2Fclient.crt |
Client certificate for mTLS (paired with --tls-ca-cert-file on the server). |
ssl_keyfile |
path | (none) | ?ssl_keyfile=%2Fetc%2Fmoon%2Fclient.key |
Client private key for mTLS. |
socket_timeout |
duration, seconds (float) | client-library default | ?socket_timeout=5 |
Per-operation read/write timeout after the connection is established. |
socket_connect_timeout |
duration, seconds (float) | client-library default | ?socket_connect_timeout=2 |
Bounds the initial TCP + (if moons/rediss) TLS handshake. See Design-for-failure β this is the knob that makes a moons:// fail-fast guarantee concrete. |
Moon-native parametersΒΆ
| Parameter | Type | Default | Example | Semantics |
|---|---|---|---|---|
workspace |
string β UUID (v7, from WS CREATE) or workspace name |
(none β connection is unbound, matching current WS AUTH behavior) |
?workspace=0193a9f2-e456-7890-abcd-ef1234567890 or ?workspace=myapp |
Sent as WS AUTH <workspace> immediately after AUTH/SELECT, before the first application command. Implementation note (R6c): the wire command WS AUTH <ws_id> today (src/command/workspace.rs) accepts only a UUID; it does not resolve names even though WorkspaceRegistry::get_by_name (src/workspace/registry.rs:58) already exists server-side. R6c must do ONE of: (a) extend WS AUTH to accept a name and resolve it server-side via get_by_name, or (b) restrict this query parameter's accepted grammar to UUID-only and document that name resolution is a client-side, pre-connect lookup. Recommendation: (a) β the registry lookup already exists, and requiring callers to know UUIDs defeats the ergonomic point of a human-readable tenant name in a connection string. This spec does not pick a winner; R6c's PR description must record the decision. |
Design-for-failureΒΆ
Per this repo's IO-failure design rule (timeouts, retries, circuit breakers, no silent degradation), the URI scheme has hard failure semantics β there is no "best effort" mode:
- No opportunistic downgrade. A
moons://target that answers in plaintext (or fails the TLS handshake in a way indistinguishable from "this port speaks plaintext") is a hard connection error, never a silent fallback to unencryptedmoon://semantics. This closes the STARTTLS-strip downgrade vector (an on-path attacker cannot force a client down to plaintext by intercepting the handshake). - No auto-upgrade, symmetrically.
moon://never opportunistically negotiates TLS even if the target happens to also accept it on the same port. Scheme selects transport; transport is never inferred from the peer's behavior. - Fail fast β never hang.
moons://dialed against a server with no--tls-portconfigured (or the wrong port) must fail within?socket_connect_timeout=(or the client's default) with a diagnostic equivalent to:
This is a connect-time classification, not a post-handshake timeout: the client
should not need to wait out a full TLS handshake timeout to learn the port doesn't speak
TLS at all when that can be determined earlier (e.g. the peer resets/closes on a raw
ClientHello, or responds with a plaintext RESP error/greeting).
- Unknown scheme β immediate parse error. Any scheme other than moon, moons,
redis, rediss is rejected at parse time, before any socket is opened. No guessing,
no "try both."
- Bounded connect, always. ?socket_connect_timeout= (or the client's configured
default when the query parameter is absent) bounds the dial for both schemes and
both transports. Retry/backoff policy is unchanged by the scheme β it is the client's
existing policy, not something the URI itself encodes (there is deliberately no
?retries=/?backoff= parameter; conflating connection addressing with retry policy
has caused ambiguity bugs in other ecosystems' URI schemes and is out of scope here).
- Malformed percent-encoding, missing mandatory moons:// port, or an unrecognized query
key are all parse errors raised before any I/O β never partially-applied, never
defaulted-and-continue.
Server participationΒΆ
Once R6a lands, the server is not just a URI target β it advertises and consumes its own scheme:
--announce-url moon(s)://host:portβ a new config flag (not present insrc/config.rstoday) giving the server a canonical externally-reachable URL. When set, it takes precedence over the existing--announce-ip/discovered-address logic for anything that currently emits a barehost:portpair.INFO replicationgains amaster_announce_urlfield carrying this value verbatim (todayINFO replicationreportsrole:master/role:slaveplus host/port fields β seesrc/replication/*.rsβ with no scheme-qualified URL).- Cluster redirects (
MOVED,ASK,CLUSTER SHARDS) surfacemoon(s)://alongside the existing bare-address form once cluster mode understands the scheme (v0.8.0 β out of scope for R6, tracked so this doc doesn't over-promise). REPLICAOF/CLUSTER MEET-adjacent inputs acceptmoon(s)://host:portas an alternative to the current bareREPLICAOF host portform (src/command/connection.rs:639).moons://selects the TLS replication connector,moon://the plaintext one β the same no-downgrade/no-upgrade rule from Design-for-failure applies to inter-node replication links, not just client connections.
Worked examplesΒΆ
# Plaintext, default port, no auth, no db-select
moon://localhost/
# Plaintext, explicit port, db 3
moon://localhost:6399/3
# TLS 1.3, explicit tls-port (mandatory β no implicit default)
moons://cache.internal:6380/
# Auth (user + password) + db-select
moon://appuser:s3cr3t@cache.internal:6379/2
# Password-only auth (Redis single-arg AUTH form)
moon://:s3cr3t@cache.internal:6379/
# TLS with explicit peer verification + CA bundle
moons://cache.internal:6380/?ssl_cert_reqs=required&ssl_ca_certs=%2Fetc%2Fmoon%2Fca.pem
# mTLS: client cert + key, CA bundle, bounded connect
moons://cache.internal:6380/?ssl_ca_certs=%2Fetc%2Fmoon%2Fca.pem&ssl_certfile=%2Fetc%2Fmoon%2Fclient.crt&ssl_keyfile=%2Fetc%2Fmoon%2Fclient.key&socket_connect_timeout=2
# Moon-native: workspace selection by UUID, plaintext
moon://appuser:s3cr3t@cache.internal:6379/0?workspace=0193a9f2-e456-7890-abcd-ef1234567890
# Moon-native: workspace selection by name, TLS
moons://appuser:s3cr3t@cache.internal:6380/0?workspace=myapp
# Server self-announce (--announce-url), as it would appear in INFO replication
master_announce_url:moons://replica-2.internal:6380
# redis:// / rediss:// keep working unmodified β parity, not deprecation
redis://appuser:s3cr3t@cache.internal:6379/0
rediss://cache.internal:6380/?ssl_cert_reqs=required
Conformance (v0.7.0 R6 implementation gate)ΒΆ
The R6 implementation (src/uri.rs + call sites) and tests/uri_scheme.rs MUST satisfy
every item below before this scheme is considered shipped. This list is the acceptance
checklist for R6, not aspirational:
-
moon://andredis://parse to byte-identical internal representations for every overlapping field (transport=plaintext, host, port, auth, db, shared query params). -
moons://andrediss://parse identically for every overlapping field (transport=TLS) except default-port behavior, which is intentionally different (see grammar) and must be a distinct, explicitly-tested case. - Round-trip: parse β re-serialize β parse is stable (idempotent) for every worked
example above, for both
moon(s)andredis(s)families. -
moons://with no port present is a parse error, not a fallback to any default. -
moons://host:<port-not-equal-to-tls-port>connects, completes TCP, and fails the TLS handshake or is rejected β verified as the exact "no opportunistic downgrade" error, not a hang and not a silent plaintext fallback. -
moons://against a server started without--tls-portfails within?socket_connect_timeout=(test asserts wall-clock bound, not just eventual failure) with the diagnostic text from Design-for-failure. -
moon://against amoons-only listener does not upgrade β either connection refused/reset or a decodable-but-hard error, never treated as a successful plaintext session against a TLS port. - Unknown scheme (
redi://,moon2://, empty scheme) is a parse error raised before any socket syscall β assert via a mock/no-network unit test, not an integration test (must not depend on network reachability to prove "never dials"). - Unrecognized query key is a parse error (not silently dropped) β one test per family.
-
?workspace=<uuid>lands the session in that workspace (WS AUTHobservably applied β e.g. a subsequentSET/GETround-trips through the workspace-prefixed keyspace) before any application command the caller issues is processed. -
?workspace=<name>behaves per whichever of the two options in the workspace parameter row R6c actually implements β the chosen behavior must itself be covered by a test, and the PR description must state which option was chosen (this doc intentionally leaves it open). -
moon-cli -u moon://β¦andmoon-cli -u moons://β¦parse-and-dial parity with the existing-u redis://β¦/-u rediss://β¦paths (same flag, wider scheme set). -
src/uri.rshas acargo-fuzztarget (any new parser needs one per CLAUDE.md) β added tofuzz/fuzz_targets/and wired into the 15-min-per-target PR fuzz job. -
--announce-url moon(s)://host:portis validated at startup (scheme β {moon, moons}, port present) and rejected with a startup error (not accepted-then-silently-ignored) if malformed. -
INFO replicationexposesmaster_announce_urlonly when--announce-urlis set; absent otherwise (no empty-string field).
Implementation-notes appendixΒΆ
Pointers into the current codebase for whoever picks up R6 β verified against this checkout, not the roadmap prose:
- TLS flags (
src/config.rs; confirmed via#[arg(long = ...)]clap attributes): --tls-port(u16, default0= disabled) βsrc/config.rs:264--tls-cert-fileβsrc/config.rs:268--tls-key-fileβsrc/config.rs:272--tls-ca-cert-file(enables mTLS / client-cert verification when set) βsrc/config.rs:276--tls-ciphersuites(comma-separated; defaults to a frozen AEAD-only, PFS-required allowlist if omitted β seeDEFAULT_CIPHER_SUITESinsrc/tls.rs) βsrc/config.rs:280- TLS engine: pure-Rust,
rustls+aws-lc-rscrypto provider (no OpenSSL dependency), TLS 1.3 by default viawith_safe_default_protocol_versions(); TLS 1.2 cipher suites are in the resolver (src/tls.rs:9-57) for interop but the default allowlist favors TLS 1.3. Seedocs/guides/tls.mdfor the operator-facing setup guide anddocs/runbooks/tls-cert-rotation.mdfor rotation. - mTLS:
build_tls_config()insrc/tls.rs:64-175builds aWebPkiClientVerifierfrom--tls-ca-cert-filewhen present; omit that flag and the server accepts any client (with_no_client_auth()). - Hot reload:
SharedTlsConfig = Arc<ArcSwap<rustls::ServerConfig>>(src/tls.rs:182); SIGHUP re-reads cert/key/CA from disk and atomically swaps (src/tls.rs:189-β¦, plus the signal-handling thread wiring later in the same file). In-flight handshakes keep the old config; new connections see the reload immediately. This is why amoons://client observing a mid-session cert rotation is expected β it is not a downgrade, the transport never changed. - Workspaces: command surface is
WS CREATE|DROP|AUTH|INFO|LIST(src/command/workspace.rs), intercepted before normal dispatch (same pattern asTXN.*/TEMPORAL.*).WS AUTH <ws_id>currently requires a UUID (validate_ws_auth,src/command/workspace.rs:106);WorkspaceRegistry::get_by_name(src/workspace/registry.rs:58) already exists but is not wired toWS AUTHβ this is exactly the R6c open decision flagged in the workspace parameter row above. See Workspaces guide for full command semantics and key-rewriting behavior. --announce-url/src/uri.rs/ and theREPLICAOF host port-only surface (src/command/connection.rs:639) confirm this is entirely prospective work β a repo search forannounce_url,announce-url, andsrc/uri.rsreturns no matches in this checkout. This document is the spec R6 must be built against; do not treat any mention of these names elsewhere as already-implemented until R6 lands.
Discrepancies vs. ROADMAP.md Β§8.5ΒΆ
Flagged during authoring, for the R6 implementer's awareness β none of these change the decided design, they are precision gaps in the roadmap's prose that this doc resolves:
?workspace=<tenant>wire semantics were unspecified. Β§8.5's parity table says the parameter "selects the Moon workspace before the first command" but doesn't say what value the client sends over the wire. The actualWS AUTHcommand (src/command/workspace.rs) takes a UUID only, not a name, even thoughWS CREATEreturns UUIDs from human-readable names and the registry already supports nameβmetadata lookup. Resolved above by making?workspace=accept either form in the URI grammar and calling out the two implementation options for R6c explicitly, rather than silently assuming name resolution "just works" today.--announce-url,INFO replication'smaster_announce_url, andsrc/uri.rsare described in Β§8.5's "Deliverables" as R6 work but the wording could be misread as already-partial. Confirmed via repo search: none exist in the current tree. This doc treats them as 100% prospective.