Compare commits

...
Author SHA1 Message Date
jakman85andClaude Opus 5 c59a4b76bb docs(crafty): a player has joined -- the allowlist chain is closed, recommendation now PROVEN
gate / gate (pull_request) Successful in 1m35s
gate / conformance (pull_request) Has been skipped
Operator added one gamertag and the Bedrock client connected and joined. The
three-pass investigation closes with a working server, no manifest change, no
transport change, no network change.

Moved out of UNVERIFIED:

* A successful join -- OPERATOR-REPORTED (I did not watch it; the distinction is
  kept deliberately). Corroborated directly on disk, which is stronger than the
  report alone: allowlist.json went [] -> one entry, 75 bytes, and that entry's
  xuid is POPULATED (mtime 22:03:17). audit.log shows the operator wrote only the
  name (Edited file .../allowlist.json, 22:02:13). Vendor doc: xuid "will be
  populated when someone with a matching name connects". So the server wrote the
  XUID on connect, ~64s after the edit -- server-authored evidence of an
  XBL-authenticated join.
* The reasoned mapping of "You are not invited" to the empty allowlist is
  CONFIRMED: one gamertag added, one successful join. allow-list=true with
  allowlist.json == [] was the wall, and nothing else was.
* transport=raknet survives a restart: the fix involved one (pid 47006 -> 47875);
  afterwards server.properties mtime is still 21:33:31 with transport=raknet, UDP
  4ABC bound, still NO TCP 4ABC, and a fresh pong from 192.168.2.22:19132 at
  22:06.
* The operator's actual route was file-edit + restart (route 2), not the console
  `allowlist add` I listed first. Both proven; console still preferred (no restart).

README: join-gate section marked RESOLVED; decision heading and option 1 promoted
from provisional to PROVEN; Bedrock human-test 4 rewritten as a 5-step verified
connect procedure, each step with its distinct failure signature, stating plainly
that the TRANSPORT TYPE ERROR banner and the "too few fields" warning BOTH appear
on a fully working server -- that pair misled two consecutive sessions.

NOT weakened, and sharpened where possible:

* auto_start=0 / crash_detection=0 STILL 0, re-measured at 22:06 after the join
  and after the operator's 22:02:28 "modified the server" action, which did not
  change them. The failure mode is now worse, not better: "worked, then vanished".
  Merging fix/crafty-oauth2-proxy-signin is such a restart.
* Silent-reset now precisely characterised: a RESTART preserves both files
  (observed) but leaves the server down (auto_start=0); a server RECREATE or
  reinstall silently resets server.properties to transport=nethernet AND
  allowlist.json to [].
* L2/L3 conformance still uncertified; multi-player SNAT still untested (exactly
  one player has joined -- that is precisely the evidence that does not settle it);
  BDS console output still never seen (log_path is '').

No manifest touched, in any of the three passes. Read-only throughout.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-29 22:08:54 -05:00
jakman85andClaude Opus 5 cd2617e5b3 docs(crafty): "You are not invited" is an empty allowlist -- and it proves RakNet works
gate / gate (pull_request) Successful in 1m36s
gate / conformance (pull_request) Has been skipped
New operator observation: a Bedrock client got "Unable to connect / You are not
invited to play on this server". That was put to me as evidence that NetherNet
signalling is working, that the refusal may have come from Microsoft rather than
our server, and that BDS players therefore never traverse crafty-l4-udp.
Measured read-only at 21:56, all three are wrong.

  allow-list=true            (server.properties line 37)
  allowlist.json == []       (3 bytes, mtime 20:51, never edited since create)
  online-mode=true
  transport=raknet           (line 71; file mtime STILL 21:33:31)
  UDP 19132 bound            (/proc/net/udp 00000000:4ABC, pid 47006)
  TCP 19132 ABSENT           (no 4ABC in /proc/net/tcp or tcp6)
  bedrock_server pid 47006, same process since 21:34, never restarted

1. transport is still raknet -- nobody reverted it, the binary did not rewrite it
   (mtime unchanged, audit.log records no later edit, same pid). NetherNet is NOT
   in use and cannot be what served this client.
2. NetherNet could not have served it in principle: it signals over TCP on
   server-port, and there is no TCP 19132 listener in the pod at all.
3. So the refusal came from OUR server, over RakNet, over 19132/udp, through the
   VIP. The 19132-19134/UDP exposure is the opposite of inert -- it carried both
   the earlier pong and this refusal.
4. Cause is certain and trivial: allow-list=true with an empty allowlist.json
   refuses every player. The shipped how_to says so, and it keys on Xbox Gamertag.

The requested log correlation cannot be done from disk: K3s-Bedrock's log_path in
crafty.sqlite is the empty string and Dedicated_Server.txt is 0 bytes -- Crafty
persists no BDS stdout at all, the console is panel-UI-only. commander.log
21:39-21:56 holds only homelab crash-detection and raknet_ping lines. That silence
is evidence about Crafty's logging, not about the refusal's origin.

Adds a README section on the join authorization model: the measured table, the
proof the refusal is ours, the "console is not on disk" caveat, gamertag/XUID
identity, and three operator routes (console `allowlist add`, edit-then-`allowlist
reload`, or allow-list=false -- discouraged, since the game port is deliberately
un-gated so the allowlist is the only join control). Notes allowlist.json is PVC
state that resets to [] on a server recreate. Options table and human test 4 now
check the allowlist before blaming the network.

Ranking unchanged at the top: keep transport=raknet, add gamertags. Still zero
manifest change. NetherNet would ADD work (TCP 19132 in crafty-l4-tcp and in
crafty-allow-ingress, plus server-udp-ports) and admit not one extra player.

Read-only throughout: allowlist.json, server.properties and the DB were read; no
console command issued, no kubectl mutation, no flux reconcile.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-29 21:59:15 -05:00
jakman85andClaude Opus 5 8697702d51 docs(decisions-log): record the gate result -- rc 3, and why CI's green is not rc 0
The handoff entry asserted conclusions but did not carry its own gate numbers.
Adds them, per CLAUDE.md section 7:

  lint / l0 / render-golden / l1 / spec-immutability : green
  gitea-settings  : SKIPPED (SETTINGS_API_TOKEN unset)
  conformance L2/L3: SKIPPED (CLUSTER_AVAILABLE != true)
  => exit 3, UNDETERMINED. Not a PASS.

Also records two things future sessions will otherwise re-learn the hard way:

1. spec-immutability skipped SPURIOUSLY from the worktree -- dev.sh looks for the
   spec repo at $REPO_ROOT/../home-network-k3s, which from a worktree is the wrong
   directory; it exists beside the MAIN checkout. Mounting it RO through the ENGINE
   shim turned the one tolerated skip into a real green, so R-TEST-06 is genuinely
   verified. The CLAUDE.md section 3 shim should mount two paths, not one.

2. CI's green tick does NOT mean rc 0. gate/conformance is `if: false` and reports
   success with "Has been skipped"; gate/gate runs
   `./ci/gate.sh || [ "$?" -eq 3 ]`, so CI tolerates exit 3 by declared design and
   would report the same green for the rc 3 above.

Branch protection and the runner were measured against the API, not assumed:
protected=true, user_can_push=false, required_approvals=1, context
'gate / gate (pull_request)', runner incluster-dind online. PR #23 left unmerged.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-29 21:53:56 -05:00
2 changed files with 408 additions and 15 deletions

No files matched your search

+255
View File
@@ -5442,6 +5442,46 @@ conclusion rather than a cautious one.
pings from the toolbox.
- **`fix/crafty-oauth2-proxy-signin` is still unmerged**, so the pod still runs the pre-that-PR spec.
### Gate and CI — rc 3, and why CI's green does not contradict that
Local, pinned toolbox, from the worktree (`ENGINE` shim + `DEV_KUBECONFIG=/dev/null`), diff base
`origin/main @ 20ea70d` → `HEAD @ d29a73c`:
| Stage | Result |
|---|---|
| `lint` | green |
| `l0` | green — 19 cases, 0 failures, 0 skips |
| `render-golden` | green — all 10 onboarded goldens reproduce |
| `l1` | green — 3 cases, 0 failures, 0 skips |
| `spec-immutability` | **green** (see below) |
| `gitea-settings` | **SKIPPED** — `SETTINGS_API_TOKEN` unset |
| `conformance` (L2/L3) | **SKIPPED** — `CLUSTER_AVAILABLE != true` |
**Exit code 3 — UNDETERMINED, not a PASS.** Nothing failed; nothing outside the green rows is
certified. Touched requirements `R-DNS-01 R-NET-01 R-PLAT-02 R-STATE-01` remain L2/L3 UNVERIFIED.
`spec-immutability` — normally *the* tolerated skip — was closed rather than accepted. It had skipped
**spuriously**: `dev.sh` resolves the spec repo at `$REPO_ROOT/../home-network-k3s`, which from a
*worktree* is the wrong directory; the repo exists beside the **main** checkout. Mounting it read-only
through the `ENGINE` shim turned the skip into a real green, so R-TEST-06 is genuinely verified here.
Worth folding into the CLAUDE.md §3 worktree workarounds: the documented shim should mount **two**
paths, the main `.git` *and* the sibling spec repo.
CI on `d29a73c`, statuses read by description and not by tick:
context='gate / gate (pull_request)' status='success' description='Successful in 1m35s'
context='gate / conformance (pull_request)' status='success' description='Has been skipped'
**Neither green means rc 0.** `gate / conformance` is `if: false` in `.gitea/workflows/gate.yml` — it
never runs and reports `success` with "Has been skipped", the exact trap CLAUDE.md §2 names. And
`gate / gate`, the single required context, runs `./ci/gate.sh || [ "$?" -eq 3 ]` (line 134): **CI
tolerates exit 3 by declared design**, so its green means "exited 0 *or* 3". It would report this
identical green for the rc 3 above. Enforcement itself was measured directly rather than assumed
(§2b): `branches/main` → `protected: true`, `user_can_push: false`, `required_approvals: 1`,
`enable_status_check: true`, `status_check_contexts: ['gate / gate (pull_request)']`;
`admin/actions/runners` → 1 runner, `incluster-dind`, `online`. **PR #23 is open, unmerged, and I did
not merge it.**
### Operator actions required
1. **Nothing to do about the transport.** `transport=raknet` is correct, live, and answering. **Do not
@@ -5463,3 +5503,218 @@ conclusion rather than a cautious one.
then re-ping. The vendor default is `nethernet`, and the regression is silent.
6. **If a second simultaneous player evicts the first**, that is the SNAT/UDP-conntrack theory becoming
real; it needs its own measured investigation, not a guess.
## 2026-09-29 (later still) — "You are not invited": the join gate is an empty allowlist. RakNet is confirmed working *by the refusal itself*
A Bedrock client reported **"Unable to connect / You are not invited to play on this server"**. This was
put to me as evidence that NetherNet signalling is working, that the refusal might have come from
Microsoft rather than our server, and that BDS players therefore never traverse this repo's
`crafty-l4-udp` path. **Measured, all three of those are wrong.** The refusal is ours, it came back over
`19132/udp`, and it has a one-line cause.
### Measured 21:56 (read-only)
| Property / file | Value |
|---|---|
| `allow-list` | **`true`** (line 37 of `server.properties`) |
| `allowlist.json` | **`[]`** — 3 bytes, mtime **20:51**, never edited since the server was created |
| `online-mode` | `true` |
| `transport` | **`raknet`** (line 71); `server.properties` mtime **still 21:33:31** |
| UDP 19132 | bound — `/proc/net/udp` `00000000:4ABC`, pid 47006 |
| **TCP 19132** | **absent** — `grep 4ABC /proc/net/tcp /proc/net/tcp6` → no match |
| `bedrock_server` | pid **47006**, etime 21:58 — the *same* process since 21:34, never restarted |
| `K3s-Bedrock.log_path` | **`''`** (empty string) in `crafty.sqlite`; `Dedicated_Server.txt` is **0 bytes** |
### What that settles
1. **`transport` is still `raknet`. Nobody reverted it and the binary did not rewrite it.** The file's
mtime is unchanged from the operator's 21:33:31 edit, `audit.log` records no later edit, and pid
47006 has been the same process since 21:34. **So NetherNet is NOT in use and cannot be what served
this client.** The premise that "something is evidently set to nethernet" is refuted.
2. **NetherNet could not have served it even in principle:** nethernet signals over a **TCP** socket on
`server-port`, and there is **no TCP 19132 listener in the pod at all**. No signalling endpoint
exists, and none is exposed by any Service.
3. **Therefore the refusal came from our server, over RakNet, over `19132/udp`, through the VIP.** A
refusal authored by the server requires the client to have reached the server; the only listener is
the RakNet UDP socket. **The 19132-19134/UDP exposure is the opposite of inert — it is the path that
carried both the earlier pong and this refusal.** *(Observed: the listeners, the missing TCP socket,
`transport=raknet`, the process identity. Reasoned from those: that the refusal traversed that path.)*
4. **The cause is trivial and certain: `allow-list=true` with `allowlist.json == []` refuses every
player.** The shipped `bedrock_server_how_to.html` states it — *"If the `allow-list` property is
enabled … the server will only allow selected users to connect. To allow a user to connect you need
to know their Xbox Live Gamertag."* An empty array selects nobody. **This configuration cannot admit
anyone, so the refusal needed no other explanation.**
### The correlation I was asked for, and why it cannot be done from disk
I was asked to correlate the client error against the BDS server log and `commander.log` at 21:40–22:00,
on the basis that our log showing nothing would mean the refusal came from Microsoft. **That inference
does not hold here, because there is no BDS log on disk to be silent.** ``K3s-Bedrock``'s `log_path` is
the empty string and `Dedicated_Server.txt` is 0 bytes: **Crafty persists no Bedrock stdout anywhere on
the PVC** — the console is panel-UI-only. `commander.log` 21:39–21:56 holds nothing but `homelab`
crash-detection lines and `raknet_ping` chatter: no join, no gamertag, no XUID, no allowlist denial.
That silence is evidence about Crafty's logging, **not** about the refusal's origin. **UNVERIFIED and
only observable live in the panel console.** The point stands: absence of a log here is not absence of
an event.
I also did **not** capture the player's packet (no `tcpdump`, no `cilium monitor`), so the flow's
identity is inferred from the listener topology, not seen. What removes the doubt is that the
configuration is *unconditionally* refusing: whichever path a client takes, `allow-list=true` plus `[]`
denies it. The operator action is required either way.
### Correction to the earlier entry's loose end, now firmer
The previous entries left "who sent those 26 packets / 27660 bytes on the `19132/UDP` policy map"
UNVERIFIED, and it stays UNVERIFIED — but note it was measured *before* 21:34, i.e. during the nethernet
window when **no UDP listener existed**. Those packets are consistent with clients RakNet-pinging a port
nothing was listening on. They should **not** be attributed to a successful Bedrock session, and equally
should not now be re-attributed to this refused client, which arrived later.
### Ranking after this measurement — unchanged at the top
**Keep `transport=raknet`, add the players to the allowlist. Still zero manifest change.** The allowlist
refusal is orthogonal to transport: it would refuse identically under NetherNet. Switching to NetherNet
would *add* manifest work (TCP 19132 in `crafty-l4-tcp` **and** `crafty-allow-ingress`, plus
`server-udp-ports` pinned and exposed) and would admit **not one extra player**. "Accept NetherNet" does
**not** outrank the status quo on this evidence, because NetherNet is not what is working — RakNet is.
Geyser stays at #2, reserve-only.
### What changed in this repo
`platform/crafty/README.md` — added *"You are not invited to play on this server — the join gate is the
allowlist, not the network"*: the measured table, the proof that the refusal is ours and rode the UDP
path, the "BDS console is not on disk" caveat, the gamertag/XUID identity model, the three operator
routes (`allowlist add` in the console; edit-then-`allowlist reload`; or `allow-list=false`, discouraged
because the game port is deliberately un-gated so the allowlist is the *only* join control), and a note
that `allowlist.json` is PVC state that resets to `[]` on a server recreate. Updated the options table
and human test 4 to put the allowlist check before blaming the network. **No manifest touched.**
### Still UNVERIFIED
- **No successful join.** Reachability and an authorization refusal are both proven; a completed login is
not. Only a human with a client, after an allowlist entry, can close it.
- **The exact client-side string was not observed by me** — it is the operator's report, and its mapping
to the allowlist denial is **reasoned** from the vendor doc, not read out of a BDS log (there is none).
- **Whether the client used Add-Server-by-IP or an Xbox/Friends route is unknown**, so a *concurrent*
Xbox-side "not invited" cannot be fully excluded. It does not matter operationally: the allowlist must
be fixed regardless, and after that a retry distinguishes the two immediately.
- **The multi-player SNAT question** remains open and untested.
- **Nothing was written.** `allowlist.json`, `server.properties` and the DB were read only; no
`allowlist add` was issued, no console command sent, no `kubectl` mutation, no `flux reconcile`.
### Operator actions — revised, allowlist first
1. **Add each player's Xbox Gamertag to the allowlist.** Crafty → `K3s-Bedrock` → Console:
`allowlist add <Gamertag>` (quote if it contains a space). Takes effect immediately, no restart.
Verify with `allowlist list`. This is **the** fix for "You are not invited".
2. **Then retry the join** from a Windows/mobile client: **Add Server** →
`crafty-mc.home.keithjackson.dev`, port **19132**. If it now connects, everything in these three
entries is closed. If it still refuses, capture the **panel console** output during the attempt —
that is the only place BDS's own reason is visible.
3. **Do not set `allow-list=false`** as the fix. The game port is deliberately un-gated at the network
layer, so the allowlist is the only thing deciding who may join. (And console `allowlist off` does not
change `server.properties`, so it silently reverts on restart.)
4. **Leave `transport=raknet` alone**, and keep ignoring both the `TRANSPORT TYPE ERROR` banner and the
`too few fields` warning — the server is reachable, proven by pong and by this refusal.
5. **`auto_start=0` / `crash_detection=0` on `K3s-Bedrock` — still outstanding.** `PATCH
/api/v2/servers/c5837301-12b1-404e-b50e-70eafb37226b`, both true, **read them back**.
6. **Treat `allowlist.json` and `transport=raknet` as backup-relevant PVC state.** A server recreate
resets the allowlist to `[]` and the transport to `nethernet` — both silently.
## 2026-09-29 (closing) — a player has joined. The chain is closed: `allow-list=true` + an empty allowlist was the wall, and nothing else was
The operator added one gamertag to the allowlist and the Bedrock client connected and joined. **The
three-entry investigation above is now closed by a working server**, with no manifest change, no
transport change, and no network change.
### What is now OBSERVED (moved out of UNVERIFIED)
- **A successful join.** *Operator-reported, 2026-09-29* — they added the player's gamertag and the
client connected and joined. **I did not watch the join**, and that distinction is kept deliberately;
this repo has been burned by collapsing it.
- **Corroborated directly on disk, though**, which is stronger than the report alone:
`allowlist.json` went `[]` → **one entry**, 75 bytes, and that entry's **`xuid` is populated**
(`keys=['ignoresPlayerLimit','name','xuid']`, mtime **22:03:17**). `audit.log` shows the operator wrote
only the name — `Edited file …/allowlist.json` at **22:02:13**. The vendor doc: `xuid` is *"Optional.
If it's not set then it will be populated when someone with a matching name **connects**."* So the XUID
was written **by the server, on connect**, ~64 s after the operator's edit. That is server-authored
evidence of an Xbox-Live-authenticated join. *(Observed: file state, both timestamps, the audit line.
Reasoned from the vendor doc: populated XUID ⇒ a connect happened.)*
- **The reasoned mapping is confirmed.** The client's *"You are not invited to play on this server"* was
mapped to the empty allowlist **by reasoning** from the vendor doc, because Crafty persists no BDS
stdout and there was no log to read it from. Adding **exactly one** gamertag and getting **exactly one**
successful join confirms that mapping. **`allow-list=true` + `allowlist.json == []` was the wall, and
nothing else was.**
- **`transport=raknet` survives a restart.** The fix involved one: `stop_server` 22:02:37 →
`start_server` 22:02:41, pid **47006 → 47875**. Afterwards `server.properties` mtime is **still
21:33:31** with `transport=raknet`, UDP `4ABC` is bound by the new pid, there is still **no TCP 4ABC**,
and `192.168.2.22:19132` answered a fresh pong at 22:06 (`MCPE;…;1.26.52;0;10;…`, new `server_guid`
matching the new pid, `players=0` — the player had left).
- **The route the operator used was file-edit + restart**, not the console `allowlist add` I put first:
`audit.log` edit 22:02:13 → stop → start. A restart is a valid substitute for `allowlist reload`.
Both routes now documented as proven; the console route is still preferred because it needs no restart.
### The recommendation is now PROVEN, not provisional
**Keep `transport=raknet` and enumerate players in the allowlist. Zero manifest change.** This is no
longer the best-reasoned option — it is the configuration a player has demonstrably joined over
`192.168.2.22:19132`. `service-l4.yaml`, `networkpolicy.yaml` and the `containerPort`s needed nothing.
Ranking otherwise unchanged: Geyser #2 (reserve, and it cannot share 19132), pinning BDS #3,
PocketMine/Nukkit #4, accept-NetherNet last — NetherNet is still **not in use**, would still need TCP
19132 exposed in two places, and would still admit not one extra player.
### Still open — NOT weakened by the success
- **`K3s-Bedrock` still has `auto_start=0` and `crash_detection=0`.** Re-measured from `crafty.sqlite` at
22:06, *after* the successful join and after the operator's 22:02:28 `modified the server` action —
which did **not** change them. **The operator now has a working server that will silently not come back
after any pod restart**, and merging `fix/crafty-oauth2-proxy-signin` **is** such a restart
(`strategy: Recreate`, RWO volume) and would stop **both** Minecraft servers. This matters *more* now,
not less: the failure mode has changed from "never worked" to "worked, then vanished".
- **The silent-reset exposure is unchanged, and now precisely characterised:**
a **restart** preserves `transport=raknet` and `allowlist.json` (observed above) but does not restart
the server itself (`auto_start=0`); a **server recreate or reinstall** resets `server.properties` to
`transport=nethernet` (nothing bound on 19132/udp) **and** `allowlist.json` to `[]` (everyone refused).
Neither reset announces itself, and the panel will show the server healthy and started. Both files are
PVC state Flux cannot restore. **Re-run the 5-step connect procedure after any recreate or BDS update.**
- **L2/L3 conformance is still uncertified.** `CLUSTER_AVAILABLE` is unset, so the gate's `conformance`
stage does not run; `R-DNS-01 R-NET-01 R-PLAT-02 R-STATE-01` remain UNVERIFIED in CI. A player joining
is not a conformance test.
- **Multi-player SNAT/UDP-conntrack is still untested.** Exactly **one** player has joined. Whether a
*second simultaneous* Bedrock player evicts the first behind the shared VIP and SNAT is unmeasured, and
a single successful session is precisely the evidence that does *not* settle it.
- **I still never saw BDS's own console output** — `log_path` is `''`, `Dedicated_Server.txt` is 0 bytes.
Anything BDS itself said about the join is visible only live in the panel.
- **Consoles still cannot reach this**, and LAN/Friends discovery still cannot work behind the VIP.
- **Geyser remains entirely unmeasured**; the pinnability of a BDS version remains UNVERIFIED.
### What changed in this repo
`platform/crafty/README.md` and `DECISIONS-LOG.md`. **No manifest touched, in any of the three passes.**
- Marked the join-gate section **RESOLVED and confirmed**, with the XUID-population evidence and the
operator-reported join labelled as exactly that.
- Promoted the decision heading and option-1 row from provisional to **PROVEN**, and recorded that the
operator's actual route was file-edit + restart (route 2), with route 1 still preferred.
- Rewrote Bedrock human-test 4 as a **5-step verified connect procedure**, each step with its distinct
failure signature, and stated plainly that the `TRANSPORT TYPE ERROR` banner and the `too few fields`
warning both appear on a fully working server — that pair misled two consecutive sessions.
- Sharpened the PVC warning to distinguish **restart** (preserves both files; server stays down because
`auto_start=0`) from **recreate/reinstall** (silently resets transport *and* allowlist).
### Operator actions — one is now urgent
1. **`PATCH /api/v2/servers/c5837301-12b1-404e-b50e-70eafb37226b`** setting `auto_start` **and**
`crash_detection` true, **and read them back**. Both are still `0`. This is the only thing standing
between the working server and it disappearing at the next pod restart — including the restart that
merging `fix/crafty-oauth2-proxy-signin` will cause.
2. **Leave `transport=raknet` and the allowlist alone.** Keep ignoring the `TRANSPORT TYPE ERROR` banner
and the `too few fields` warning.
3. **Add a gamertag per additional player** — console `allowlist add <Gamertag>` (no restart needed).
Do **not** switch to `allow-list=false`: the game port is un-gated by design, so the allowlist is the
only join control.
4. **After any Bedrock recreate or BDS update**, re-run the 5-step connect procedure — transport and
allowlist both reset silently.
5. **Before announcing this to more than one player**, note that only one simultaneous session has been
proven; if a second player evicts the first, that is the SNAT/conntrack question and needs its own
measured investigation.
+153 -15
View File
@@ -181,20 +181,145 @@ one `None` terminator entry as missing. Every field that matters (edition, MOTD,
name, player count, max players, uuid, game mode, both ports) parsed correctly. **Do not chase this
warning.** The failure signature is the *other* message, `Got timeout`.
#### Decision: `transport=raknet` stays; the 19132-19134/UDP exposure is correct and live
### "You are not invited to play on this server" — the join gate is the allowlist, not the network (MEASURED 2026-09-29 21:56; **RESOLVED and confirmed 22:03**)
A Bedrock client reported **"Unable to connect / You are not invited to play on this server"**. That is
an **authorization** refusal, and it is the expected, guaranteed behaviour of this server's current
configuration. Measured:
| Property / file | Value | Source |
|---|---|---|
| `allow-list` | **`true`** (line 37) | `server.properties` |
| `allowlist.json` | **`[]`** — 3 bytes, mtime 20:51, never edited since server create | `cat allowlist.json` |
| `online-mode` | `true` (line 31) | `server.properties` |
| `transport` | **`raknet`** (line 71), file mtime **still 21:33:31** | `server.properties` |
| TCP 19132 listener | **none** (`NO-TCP-4ABC`) | `grep 4ABC /proc/net/tcp /proc/net/tcp6` |
| UDP 19132 listener | present, `00000000:4ABC`, pid 47006 | `grep 4ABC /proc/net/udp` |
**`allow-list=true` with an empty `allowlist.json` refuses every player, unconditionally.** The shipped
`bedrock_server_how_to.html` states it: *"If the `allow-list` property is enabled in server.properties
then the server will only allow selected users to connect. To allow a user to connect you need to know
their Xbox Live **Gamertag**."* An empty list selects nobody. This is not a network problem, not a
transport problem, and not a platform problem — and no amount of Service or policy work will change it.
#### CONFIRMED: one gamertag added, and a player joined
**This is no longer a hypothesis. The fix was applied and it worked.** *(The join itself is
**operator-reported** — 2026-09-29, the operator added the player's gamertag and the Bedrock client
connected and joined. I did not watch it.)* Two independent things corroborate it **directly on disk**:
- `allowlist.json` went from `[]` to **one entry**, and that entry's **`xuid` is populated**
(`keys=['ignoresPlayerLimit','name','xuid']`, file 75 bytes, mtime **22:03:17**). Per the vendor doc,
`xuid` is *"Optional. If it's not set then it will be populated when someone with a matching name
**connects**."* The operator wrote only the name — `audit.log` records `Edited file
…/allowlist.json` at **22:02:13** — so **the XUID was written by the server, on connect, at 22:03:17**.
That is server-authored evidence of an Xbox-Live-authenticated join. *(Observed: the file state and
both timestamps. Reasoned from the vendor doc: that a populated XUID implies a connect.)*
- The RakNet path still answers after the restart this involved: a pong from `192.168.2.22:19132` at
22:06, `MCPE;…;1.26.52;0;10;…` (`players=0` — the player had left by then), new `server_guid` matching
the new pid.
**So the causal chain is closed: `allow-list=true` + `allowlist.json == []` was the wall, and nothing
else was.** The earlier mapping of the client's *"You are not invited"* string to the empty allowlist was
**reasoned** from the vendor doc (there is no BDS log on disk to read it from); adding exactly one
gamertag and getting exactly one successful join **confirms that mapping**. No transport change, no
manifest change, and no network change was involved in the fix.
> **Note the route the operator actually used:** they **edited `allowlist.json` directly** and then
> **restarted** the server (`audit.log`: edit 22:02:13 → `stop_server` 22:02:37 → `start_server`
> 22:02:41; pid 47006 → **47875**). That is route 2 below, and a restart is a valid substitute for
> `allowlist reload`. Route 1 (`allowlist add` in the console) avoids the restart and is still preferred.
**This is also positive evidence that the RakNet path works end to end.** A refusal authored by *our*
server required the client to reach our server and get an answer back. The only listener on 19132 is
the **RakNet UDP** socket — there is **no TCP 19132 listener at all**, and NetherNet requires a TCP
signalling socket on `server-port`. So the client cannot have used NetherNet against this server; it
arrived over `19132/udp`, through `crafty-l4-udp` and the VIP. **The 19132-19134/UDP exposure is not
inert — it is the path that carried both the pong and this refusal.** *(Observed: the listeners, the
absent TCP socket, `transport=raknet`. Reasoned from those: that the refusal traversed that path. Not
captured: the actual player flow — see the caveat below.)*
#### Caveat: BDS console output is NOT on disk, so a join attempt cannot be correlated from the filesystem
`K3s-Bedrock`'s `log_path` in `crafty.sqlite` is the **empty string**, and `Dedicated_Server.txt` is
**0 bytes**. Crafty does not persist a Bedrock server's stdout anywhere on the PVC — the console exists
only in the panel UI. `commander.log` between 21:39 and 21:56 contains nothing but `homelab`
crash-detection lines and the `raknet_ping` chatter: **no join, no gamertag, no XUID, no allowlist
denial.** That absence is **not** evidence about where the refusal came from; it is evidence that
Crafty logs no BDS stdout at all. **To see a rejected join you must watch the panel console live.**
UNVERIFIED, and only observable in the UI.
#### How a Bedrock player actually gets authorized here
Identity is an **Xbox Live Gamertag** (`online-mode=true` forces XBL auth, which is what gives BDS a
gamertag/XUID to check). `allowlist.json` is a JSON array; per the vendor doc the keys are `name` (the
gamertag), optional `xuid` (*"populated when someone with a matching name connects"*), and
`ignoresPlayerLimit`. So the operator adds **one entry per player, keyed on gamertag** — there is no
group, role, or Keycloak integration, and **none of this touches the oauth2-proxy/Keycloak gate**,
which protects only the web panel.
Three ways to do it, all **operator actions**. **Routes 1 and 2 are both proven** — route 2 is the one
that produced the confirmed join above:
1. **Preferred — the panel console.** In Crafty, open `K3s-Bedrock` → Console and send:
`allowlist add <Gamertag>` (quote it if it contains a space: `allowlist add "Example Name"`).
This writes `allowlist.json` **and** takes effect immediately, with no restart.
2. **Edit the file, then reload.** Write `/crafty/servers/c5837301-…/allowlist.json`, then send
`allowlist reload` in the console — *"After you've modified the file you need to run the command
`allowlist reload` to make sure that the server knows about your new change."* Shape:
```json
[
{ "ignoresPlayerLimit": false, "name": "FirstPlayerGamertag" },
{ "ignoresPlayerLimit": false, "name": "SecondPlayerGamertag" }
]
```
3. **Turn the allowlist off entirely** — `allow-list=false` in `server.properties` (restart), or
`allowlist off` in the console. Note the vendor's warning: `allowlist on|off` *"does not change the
value in the server.properties file"*, so a console `off` silently reverts on the next start.
**Not recommended:** the game port is deliberately un-gated at the network layer (see *Game-port L4
exposure*), so the allowlist is the **only** thing deciding who may join. Keep it `true` and enumerate
players.
> **`allowlist.json` is application state on the PVC**, exactly like `transport=raknet`. Neither is in
> git and **Flux can restore neither**. Be precise about what breaks them, because the two cases differ:
>
> - **A pod or server *restart* preserves both** — observed: the 22:02:48 restart (pid 47006 → 47875)
> kept `transport=raknet` (`server.properties` mtime unchanged at 21:33:31) and kept the allowlist
> entry. What a **pod** restart *does* break is that `K3s-Bedrock` has `auto_start=0`, so the Bedrock
> server simply **does not come back** until someone starts it by hand.
> - **A server *recreate* or reinstall silently resets both** — a fresh `server.properties` ships
> `transport=nethernet` (nothing bound on 19132/udp) and a fresh `allowlist.json` is `[]` (everyone
> refused). Neither change announces itself, and the server will look healthy and started in the panel
> while being unreachable *and* unjoinable.
>
> **Treat both files as backup-relevant, and re-run the 5-step connect procedure above after any Bedrock
> recreate or BDS update.**
#### Why this does not change the transport recommendation
`transport=raknet` is correct, and with the allowlist entry in place the combination is **proven
end-to-end by a player who has joined** — not merely the best-reasoned option. The allowlist refusal was
orthogonal to transport all along: it would have refused players identically under NetherNet. Switching
to NetherNet would *add* work (TCP 19132 in `crafty-l4-tcp` **and** in `crafty-allow-ingress`, plus
`server-udp-ports` pinned and exposed) and would admit **not one extra player**. The ranking below stands,
and option 1 is now settled rather than provisional.
#### Decision (PROVEN, not provisional): `transport=raknet` stays; the 19132-19134/UDP exposure is correct and live
No manifest change is needed or wanted. `service-l4.yaml`, `networkpolicy.yaml` and the
`containerPort`s were already shaped for exactly RakNet, and that shape is now proven end-to-end
rather than assumed. The three options that would have required manifest work are all **not
recommended** today:
`containerPort`s were already shaped for exactly RakNet, and that shape is now **proven by a player who
has connected and joined over `192.168.2.22:19132`** — not assumed, and not merely reasoned. The options
that would have required manifest work are all **not recommended**:
| Option | Manifest consequence | Verdict |
|---|---|---|
| **Keep `transport=raknet`** (recommended) | **none** — the existing Service, policy and ports are already correct and now proven | **Take this.** Zero change, works today, vanilla parity, Bedrock clients join a real Bedrock world. |
| **Keep `transport=raknet`** + allowlist entries (**this is what works**) | **none** — the existing Service, policy and ports were already correct | **Done; proven.** A player has joined over the VIP. Zero manifest change, vanilla parity, real Bedrock world. |
| **GeyserMC + Floodgate** on the `homelab` Paper server | none either (Geyser also binds `19132/udp`), but it would **collide** with the live BDS on 19132 — one of them must move | Hold in reserve. Only worth it if RakNet is really removed, or if you *want* one shared world. Cost: Bedrock players join the **Java** world, not a Bedrock world; Java-only mechanics and some content differ; it is another moving part to patch. |
| **Pin/downgrade BDS** to a pre-nethernet release | none | Unnecessary — the *current* build already speaks RakNet. Also fragile: Microsoft removes old BDS downloads, and Crafty's Bedrock installer tracks latest. UNVERIFIED whether Crafty can pin a version at all. |
| **A third-party server** (PocketMine-MP, Nukkit) | none | No. Loses vanilla parity and the world format; a large change to solve a problem that is not occurring. |
| **Accept NetherNet** | 19132-19134/UDP becomes **inert**; nethernet would instead need **TCP** 19132 added to `crafty-l4-tcp` *and* to `crafty-allow-ingress`, plus `server-udp-ports` pinned and exposed for the negotiated media path | No. Strictly more manifest work than the option that already works, and the WebRTC media path behind a shared VIP and SNAT is unproven here. |
| **Accept NetherNet** | 19132-19134/UDP becomes **inert**; nethernet would instead need **TCP** 19132 added to `crafty-l4-tcp` *and* to `crafty-allow-ingress`, plus `server-udp-ports` pinned and exposed for the negotiated media path | No — and note it is **not currently in use**: `transport=raknet` and there is no TCP 19132 listener. Strictly more manifest work than the option that already works, it admits no extra player (the allowlist is the gate), and WebRTC media behind a shared VIP and SNAT is unproven here. |
#### If RakNet is ever really removed
@@ -261,15 +386,28 @@ Gateway VIP). The sidecar→Crafty hop is pod-local (`127.0.0.1`) and not subjec
3. **Server-create + game reach (Java):** in the Crafty UI create a Java server bound to `25565`;
start it; connect a Minecraft client to `crafty-mc.home.keithjackson.dev:25565` (or
`192.168.2.22:25565`) from the LAN and confirm the world loads.
4. **Server-create + game reach (Bedrock):** creating and starting the server is **not sufficient**.
Check, in this order (see *the transport trap* above): `transport=raknet` in `server.properties`;
a live **UDP** listener (`grep 4ABC /proc/net/udp /proc/net/udp6` → a wildcard bind); and a real
pong from off-cluster. **Ignore the `TRANSPORT TYPE ERROR` banner in the console and ignore
Crafty's `too few fields` warning — both appear on a fully working server.** The authoritative
test is a RakNet Unconnected Ping to `192.168.2.22:19132` returning a `0x1c` packet whose MOTD
string starts `MCPE;`. Then **Add Server** by hand in a Windows/mobile client at
`crafty-mc.home.keithjackson.dev` port `19132`. It will never show up in the LAN/Friends tab, and a
console cannot reach it.
4. **Bedrock: the connect procedure (VERIFIED end-to-end 2026-09-29 — a player has joined).**
Creating and starting the server is **not sufficient**. Do these in order; each step has a distinct
failure signature:
1. **`transport=raknet`** in `server.properties`. The vendor default is `nethernet`, which opens
**no UDP listener at all**.
2. **A live UDP listener:** `grep 4ABC /proc/net/udp /proc/net/udp6` → a wildcard bind. Under
`nethernet` you instead see `4ABC` on **TCP**, which nothing here exposes.
3. **A pong from off-cluster:** a RakNet Unconnected Ping to `192.168.2.22:19132` returns a `0x1c`
packet whose MOTD string starts `MCPE;`. This proves the VIP, Service and policy path.
4. **An allowlist entry for the player's Xbox Gamertag.** `allow-list=true` with
`allowlist.json == []` refuses **every** player with *"You are not invited to play on this
server"* while the server is otherwise perfectly reachable. In the panel console:
`allowlist add <Gamertag>`.
5. **Add Server** by hand in a Windows/mobile client: `crafty-mc.home.keithjackson.dev`, port
`19132`. It will never appear in the LAN/Friends tab, and a console cannot reach it at all.
**Two log messages appear on a fully working server and must be ignored:** BDS's
`================ TRANSPORT TYPE ERROR ===================` banner (advisory — see *Trap 2*), and
Crafty's perpetual `Bedrock ping returned a string with too few fields`. Neither indicates a fault;
chasing them is what cost two sessions. The **real** failure signatures are `Got timeout while
issuing bedrock ping` (no listener) and *"You are not invited"* (allowlist).
## Runtime state: the servers themselves are NOT in git