docs(crafty): Bedrock on 19132 RESOLVED — a player has joined; the wall was an empty allowlist #23

Merged
jakman85 merged 5 commits from fix/crafty-bedrock-19132 into main 2026-09-30 03:19:04 +00:00
Owner

RESOLVED — a player has joined. Two findings, both now closed.

1. transport=raknet works. BDS 1.26.52's TRANSPORT TYPE ERROR banner is advisory, not a
refusal
— the server printed it and bound the RakNet sockets anyway.

2. The wall was an empty allowlist. allow-list=true + allowlist.json == [] refused every player.
The operator added one Xbox Gamertag and the client connected and joined. (The join is
operator-reported — I did not watch it.)

Fix shipped: zero manifest change. service-l4.yaml, networkpolicy.yaml and the containerPorts
were already correct; nothing in git needed to change in any of the three passes.

How the join is corroborated on disk (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 — 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.)

This confirms the reasoning step. The mapping of "You are not invited to play on this server" to the
empty allowlist was reasoned from the vendor doc, because Crafty persists no BDS stdout and there was
no log to read it from. One gamertag added, one successful join: allow-list=true + [] was the wall,
and nothing else was.

Also newly observed: 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 (players=0 — the player had left). And the
operator's actual route was file-edit + restart (route 2), not the console allowlist add I listed
first; both are proven, console still preferred since it needs no restart.

The original two findings

1. transport=raknet works. BDS 1.26.52's TRANSPORT TYPE ERROR banner is advisory, not a
refusal
— the server printed it and bound the RakNet sockets anyway. UDP 19132 is bound and answering,
proven end-to-end from off-cluster.

2. The live blocker is authorization, not the network. A client reporting "You are not invited to
play on this server"
is allow-list=true + allowlist.json == []. An empty allowlist with the
allowlist enabled refuses every player, unconditionally.
Fix: add each player's Xbox Gamertag.

The allowlist finding, measured 21:56 (read-only)

Property / file Value
allow-list true (server.properties line 37)
allowlist.json [] — 3 bytes, mtime 20:51, never edited since server 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
K3s-Bedrock.log_path '' in crafty.sqlite; Dedicated_Server.txt 0 bytes

This refutes three inferences that were put to me, and the refutations matter:

  • "NetherNet signalling is evidently working." No. transport is still raknet — mtime unchanged
    from the operator's 21:33:31 edit, audit.log shows no later edit, same pid throughout. And NetherNet
    signals over TCP on server-port, of which there is no listener at all. NetherNet is not in
    use and could not have served this client.
  • "The refusal may have come from Microsoft, not our server." A server-authored refusal required
    reaching the server, and the only listener on 19132 is the RakNet UDP socket. (Observed: the
    listeners, the absent TCP socket, transport=raknet. Reasoned from those: the refusal rode that path.
    Not captured: the player's actual flow — no tcpdump/cilium monitor.)
    What removes the doubt is
    that the config is unconditionally refusing: whichever path a client takes, true + [] denies it.
  • "BDS players never traverse crafty-l4-udp, so the exposure is inert." The opposite. That path
    carried both the pong and this refusal. The 19132-19134/UDP exposure is live and correct.

The requested log correlation cannot be done from disk. K3s-Bedrock's log_path is the empty
string
and Dedicated_Server.txt is 0 bytes — Crafty persists no BDS stdout anywhere; the console
is panel-UI-only. commander.log 21:39–21:56 holds only homelab crash-detection and raknet_ping
chatter: no join, no gamertag, no XUID, no denial. That silence is evidence about Crafty's logging, not
about the refusal's origin.
To see BDS's own reason you must watch the panel console live.

The join model (how a player actually gets authorized)

Identity is an Xbox Live Gamertag (online-mode=true forces XBL auth). allowlist.json entries are
{name, xuid?, ignoresPlayerLimit} — xuid is "populated when someone with a matching name connects".
One entry per player; no groups, no roles, no Keycloak involvement (that gate protects only the web
panel). Operator routes, in order of preference: console allowlist add <Gamertag> (immediate, no
restart, verify with allowlist list); or edit the file then allowlist reload; or allow-list=false —
discouraged, because 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).

allowlist.json is PVC state — not in git, Flux cannot restore it, and a server recreate resets it
to []. Same class as transport=raknet. Both are backup-relevant.


Finding 1 detail: the transport banner is advisory

This PR started as "BDS is NetherNet-only, so crafty-l4-udp is inert." That premise is refuted.
The previous commit's recommendation (transport=raknet) was correct — honestly labelled REASONED, NOT
OBSERVED
, and now OBSERVED. What was wrong was reading an ERROR-level log line as a refusal without
checking what the process did next.

Evidence chain (all OBSERVED unless tagged)

# Measurement Command / source
1 ./bedrock_server pid 47006 still running 5 min after printing the banner ps -eo pid,etime,args in the crafty container
2 UDP 19132 + 19133 bound: /proc/net/udp 00000000:4ABC, /proc/net/udp6 …:4ABD. The earlier TCP bind on 4ABC is gone grep 4ABC /proc/net/udp /proc/net/udp6 /proc/net/tcp*
3 Both sockets owned by that pid /proc/47006/fd/25, fd/26 → those socket inodes
4 Crafty's warning changed kind: last Got timeout 21:34:04,306, first too few fields 21:34:09,854 (155 → 86 lines) /crafty/logs/commander.log
5 Got timeout fires only on sock.recv → TimeoutError; too few fields is reachable only after a 0x1C Unconnected Pong arrives with valid magic app/classes/remote_stats/raknet_ping.py, read
6 End-to-end from off-cluster: RakNet Unconnected Ping → 192.168.2.22:19132 returns a 139-byte 0x1c pong, magic valid, ~12 ms: MCPE;Dedicated Server;2193;1.26.52;0;10;…;Survival;1;19132;19133;0;0;0; hand-rolled RakNet ping from the toolbox on the host network
7 Vendor doc on the PVC documents raknet as supported: "the server listens directly on server-port (IPv4) and server-portv6 (IPv6), and clients connect to that address and port" bedrock_server_how_to.html, shipped in the same archive
8 RakNet still fully compiled in: RakNetConnector, createUniqueRakPeer, RakPeerInterface, …/src-deps/raknet/raknet/RakNetSocket2.cpp strings bedrock_server
9 Operator timeline: stop 21:32:32 → edit server.properties 21:33:31 → start 21:33:58 /crafty/logs/audit.log
10 K3s-Bedrock still auto_start=0, crash_detection=0 crafty.sqlite, opened read-only

So "NetherNet is the only supported transport type" is a support statement, not a capability
one: Microsoft declines to support RakNet and ships it working.

Why NetherNet could never have worked here — the finding the last pass missed

The same vendor doc explains the previous pass's oddest measurement (19132 bound TCP): nethernet
"listens on a single dual-stack TCP socket on server-port for an HTTP-based signaling
handshake."
And this repo exposes no TCP 19132 — crafty-l4-tcp is 25565-25567 only, and
crafty-allow-ingress allows 19132 UDP and not 19132 TCP. Unreachable at the Service layer and
the policy layer. The server's own packet-statistics.txt for that run agrees: Total Received: 0, 0B
over 2293 s. The 26 packets the last pass found on the 19132/UDP policy map were players' RakNet
pings hitting a port where nethernet had opened no UDP socket. Consistent, and now closed.

Recommendation, ranked — with the manifest consequence of each

Rank Option Manifest consequence Verdict
1 Keep transport=raknet none — Service, policy and containerPorts were already correct, now proven end-to-end Take this. Works today, vanilla parity, real Bedrock world.
2 GeyserMC + Floodgate on the homelab Paper server none (Geyser also binds 19132/udp) but it collides with the live BDS — one must move Reserve. Only if RakNet is really removed. Cost: Bedrock players join the Java world. Geyser's version support is web/unmeasured.
3 Pin/downgrade BDS none Unnecessary — the current build speaks RakNet. Whether Crafty can pin a version is UNVERIFIED.
4 PocketMine-MP / Nukkit none No — loses vanilla parity and world format.
5 Accept NetherNet 19132-19134/UDP goes inert; would need TCP 19132 added to crafty-l4-tcp and crafty-allow-ingress, plus server-udp-ports pinned and exposed No — strictly more work than the option that already works, and WebRTC media behind a shared VIP + SNAT is unproven here.

The 19132-19134/UDP exposure stays and is not inert. The allowlist refusal is orthogonal to
transport
— it would refuse players identically under NetherNet. So "accept NetherNet" does not
outrank the status quo: NetherNet is not what is working, RakNet is, and switching would add manifest
work while admitting not one extra player.

Changes — platform/crafty/README.md and DECISIONS-LOG.md only. No manifest touched.

  • Rewrote "the transport trap" as two traps: nethernet hides the UDP socket, then the ERROR
    banner frightens you into reverting the fix. Full evidence chain, vendor quotes, binary symbols, pong.
  • Documented that Crafty's perpetual too few fields - missing values [None] is the success
    signature: BDS 1.26.52's pong has 16 ;-fields, Crafty 4.10.8's pingpong_string_fields knows 14 and
    wants an empty terminator, gets '0'. Cosmetic. The failure signature is the other message.
  • Added the ranked options table and an "If RakNet is ever really removed" migration section.
  • enable-lan-visibility upgraded to OBSERVED and a previous claim corrected: 19133 is not
    unreachable — it answers with the same server_guid, so Bedrock slot 1 is genuinely occupied.
  • Game VIP corrected: the README asserted 192.168.1.55, which is the base/dev value.
    clusters/prod/kustomization.yaml patches CRAFTY_L4_VIP to 192.168.2.22 — the live EXTERNAL-IP
    on both L4 Services.
  • Noted transport=raknet is PVC state Flux cannot restore, and silently regresses to the vendor
    default on a Bedrock server recreate or reinstall.
  • DECISIONS-LOG.md: appended a new entry. The prior entry is not edited or deleted — it
    reasoned correctly and flagged its own uncertainty honestly; the new entry records that the
    observation vindicated it and that the misread was of the server's reaction.

Gate: rc 3 — UNDETERMINED. NOT green. Not a PASS.

ENGINE=<shim> DEV_KUBECONFIG=/dev/null ./infra/devcontainer/dev.sh run bash -c './ci/gate.sh'
from the worktree; diff base origin/main @ 20ea70d → HEAD @ c59a4b7.

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 — R-TEST-06 genuinely verified (see below)
gitea-settings SKIPPED — SETTINGS_API_TOKEN unset
conformance (L2/L3) SKIPPED — CLUSTER_AVAILABLE != true

rc 3, because two stages did not run. Nothing failed; nothing beyond the green rows is certified.
Touched requirements R-DNS-01 R-NET-01 R-PLAT-02 R-STATE-01 are L2/L3 UNVERIFIED.

spec-immutability initially skipped spuriously: 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 that skip into a real green, so
the one normally-tolerated skip is closed here rather than accepted.

Separately, and not from the gate, the enforcement seam was measured directly (CLAUDE.md §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. A green tick here still proves only the L0/L1 offline subset
— and a skipped Gitea job reports success with "Has been skipped", so read the description.

What CI reported on d29a73c (the prior head) (measured, descriptions read — not just the tick)

context='gate / gate (pull_request)'        status='success'  description='Successful in 1m35s'
context='gate / conformance (pull_request)' status='success'  description='Has been skipped'

Combined commit status: state: success. Neither tick means the gate reached rc 0.

  • gate / conformance is if: false in .gitea/workflows/gate.yml — it never runs and reports
    success with "Has been skipped". This is the exact trap CLAUDE.md §2 names.
  • gate / gate — the one required context — genuinely ran (1m35s), but the workflow invokes
    ./ci/gate.sh || [ "$?" -eq 3 ] (line 134). CI tolerates exit 3 by declared design. So its
    green means "gate.sh exited 0 or 3". My local run exited 3, and CI would report exactly
    this same green for it. The tick is not evidence of rc 0 and must not be read as one.

Still open — NOT weakened by the success

  • auto_start=0 / crash_detection=0 are still 0 (re-measured 22:06, after the join). See
    operator action 1 — this is now the urgent item.
  • The silent-reset exposure, now precisely characterised: a restart preserves transport=raknet
    and allowlist.json (observed) but leaves the server down because auto_start=0; a server recreate
    or reinstall
    silently resets server.properties to transport=nethernet (nothing bound on
    19132/udp) and allowlist.json to [] (everyone refused). Neither announces itself, and the panel
    shows the server healthy. Both files are PVC state Flux cannot restore.
  • L2/L3 conformance is still uncertified — R-DNS-01 R-NET-01 R-PLAT-02 R-STATE-01 remain UNVERIFIED
    in CI. A player joining is not a conformance test.
  • Consoles still cannot reach this; LAN/Friends discovery still cannot work behind the VIP.
  • Geyser remains entirely unmeasured; BDS version pinnability remains UNVERIFIED.

UNVERIFIED — read before acting

  • The join itself is operator-reported, not watched by me. Corroborated on disk by the
    server-written XUID (above), which is why it is listed as evidence rather than as unverified — but the
    distinction is kept deliberately.
  • Only ONE simultaneous session is proven. Whether a second concurrent Bedrock player evicts the
    first (masquerade port reuse / UDP conntrack behind the shared VIP) is untested — 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.
  • The multi-player SNAT question is still open. Whether a second simultaneous player evicts the
    first (masquerade port reuse / UDP conntrack behind a shared VIP) is untested. One ping is one flow.
  • Geyser is entirely unmeasured here — nothing installed or run. Its version support, jar names and
    config keys in the README are documentation-sourced.
  • Whether Crafty can pin a BDS version — UNVERIFIED, its installer was not read.
  • Whether a future BDS really removes RakNet — unknowable from here. The banner is a deprecation
    notice with an unknown expiry.
  • A web search on the error string was attempted and was low quality (results echoed the error
    back). Nothing here rests on it — all load-bearing evidence is local: /proc/net/*,
    /proc/47006/fd/*, audit.log, commander.log, raknet_ping.py, the shipped how-to,
    strings bedrock_server, and the pong.

Read-only, as briefed

No kubectl apply/edit/patch/delete/scale/rollout, no flux reconcile, no write anywhere inside the
container
— server.properties was read, never touched. Pod is still crafty-64f7b6d78-pkpgp
(2/2, 15d, 0 restarts). All probes were reads plus outbound UDP pings from the toolbox. Not merged.

Operator actions

  1. URGENT — PATCH /api/v2/servers/c5837301-12b1-404e-b50e-70eafb37226b setting auto_start
    and crash_detection true, and read them back. Both are still 0, re-measured at 22:06
    after the successful join and after the operator's 22:02:28 "modified the server" action, which did
    not change them. This is the only thing between the working server and it silently vanishing at the
    next pod restart — and merging fix/crafty-oauth2-proxy-signin is such a restart
    (strategy: Recreate, RWO volume), which would stop both Minecraft servers. The failure mode is
    now worse than before: "worked, then vanished" rather than "never worked".
  2. Add a gamertag per additional player — console allowlist add <Gamertag> (no restart needed),
    verify with allowlist list.
  3. Do not set allow-list=false — the game port is un-gated by design, so the allowlist is the only
    join control (and console allowlist off does not persist to server.properties).
  4. Leave transport=raknet alone. Do not revert it because of the TRANSPORT TYPE ERROR banner,
    and do not chase the too few fields warning
    — both appear on a fully working server.
  5. Before announcing to more than one player: only one simultaneous session is proven. If a
    second player evicts the first, that is the SNAT/UDP-conntrack question and needs its own measured
    investigation.
  6. (superseded by 1) auto_start PATCH /api/v2/servers/c5837301-12b1-404e-b50e-70eafb37226b
    setting auto_start and crash_detection true, and read them back (the create API drops them).
    Until then Bedrock does not survive a pod restart — and merging fix/crafty-oauth2-proxy-signin
    restarts the pod (strategy: Recreate), stopping both servers.
  7. Set enable-lan-visibility=false on K3s-Bedrock if a second Bedrock server is ever wanted —
    it currently occupies both 19132 and 19133 (measured).
  8. Re-check after any Bedrock recreate or BDS update — re-run the 5-step connect procedure; the allowlist resets to [] and: confirm transport=raknet survived, then
    re-ping. The vendor default is nethernet and the regression is silent.
  9. Optional: wire SETTINGS_API_TOKEN (infra/gitea/set-ci-token.sh) to close the gitea-settings
    skip in future gate runs.

🤖 Generated with Claude Code

## RESOLVED — a player has joined. Two findings, both now closed. **1. `transport=raknet` works.** BDS 1.26.52's `TRANSPORT TYPE ERROR` banner is **advisory, not a refusal** — the server printed it and bound the RakNet sockets anyway. **2. The wall was an empty allowlist.** `allow-list=true` + `allowlist.json == []` refused every player. The operator added one Xbox Gamertag and **the client connected and joined.** *(The join is **operator-reported** — I did not watch it.)* **Fix shipped: zero manifest change.** `service-l4.yaml`, `networkpolicy.yaml` and the `containerPort`s were already correct; nothing in git needed to change in any of the three passes. ### How the join is corroborated on disk (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 — 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.)* **This confirms the reasoning step.** The mapping of *"You are not invited to play on this server"* to the empty allowlist was **reasoned** from the vendor doc, because Crafty persists no BDS stdout and there was no log to read it from. One gamertag added, one successful join: **`allow-list=true` + `[]` was the wall, and nothing else was.** Also newly observed: **`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 (`players=0` — the player had left). And the operator's actual route was **file-edit + restart** (route 2), not the console `allowlist add` I listed first; both are proven, console still preferred since it needs no restart. ## The original two findings **1. `transport=raknet` works.** BDS 1.26.52's `TRANSPORT TYPE ERROR` banner is **advisory, not a refusal** — the server printed it and bound the RakNet sockets anyway. UDP 19132 is bound and answering, proven end-to-end from off-cluster. **2. The live blocker is authorization, not the network.** A client reporting *"You are not invited to play on this server"* is `allow-list=true` + `allowlist.json == []`. **An empty allowlist with the allowlist enabled refuses every player, unconditionally.** Fix: add each player's Xbox **Gamertag**. ### The allowlist finding, measured 21:56 (read-only) | Property / file | Value | |---|---| | `allow-list` | **`true`** (`server.properties` line 37) | | `allowlist.json` | **`[]`** — 3 bytes, mtime 20:51, never edited since server 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 | | `K3s-Bedrock.log_path` | **`''`** in `crafty.sqlite`; `Dedicated_Server.txt` **0 bytes** | This refutes three inferences that were put to me, and the refutations matter: - **"NetherNet signalling is evidently working."** No. `transport` is still `raknet` — mtime unchanged from the operator's 21:33:31 edit, `audit.log` shows no later edit, same pid throughout. And NetherNet signals over **TCP** on `server-port`, of which there is **no listener at all**. NetherNet is not in use and could not have served this client. - **"The refusal may have come from Microsoft, not our server."** A server-authored refusal required reaching the server, and the only listener on 19132 is the **RakNet UDP** socket. *(Observed: the listeners, the absent TCP socket, `transport=raknet`. Reasoned from those: the refusal rode that path. Not captured: the player's actual flow — no `tcpdump`/`cilium monitor`.)* What removes the doubt is that the config is *unconditionally* refusing: whichever path a client takes, `true` + `[]` denies it. - **"BDS players never traverse `crafty-l4-udp`, so the exposure is inert."** The opposite. That path carried both the pong and this refusal. **The 19132-19134/UDP exposure is live and correct.** **The requested log correlation cannot be done from disk.** `K3s-Bedrock`'s `log_path` is the **empty string** and `Dedicated_Server.txt` is 0 bytes — **Crafty persists no BDS stdout anywhere**; the console is panel-UI-only. `commander.log` 21:39–21:56 holds only `homelab` crash-detection and `raknet_ping` chatter: no join, no gamertag, no XUID, no denial. **That silence is evidence about Crafty's logging, not about the refusal's origin.** To see BDS's own reason you must watch the panel console live. ### The join model (how a player actually gets authorized) Identity is an **Xbox Live Gamertag** (`online-mode=true` forces XBL auth). `allowlist.json` entries are `{name, xuid?, ignoresPlayerLimit}` — `xuid` is *"populated when someone with a matching name connects"*. One entry per player; no groups, no roles, **no Keycloak involvement** (that gate protects only the web panel). Operator routes, in order of preference: console `allowlist add <Gamertag>` (immediate, no restart, verify with `allowlist list`); or edit the file then `allowlist reload`; or `allow-list=false` — **discouraged**, because 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). `allowlist.json` is **PVC state** — not in git, Flux cannot restore it, and a server recreate resets it to `[]`. Same class as `transport=raknet`. Both are backup-relevant. --- ## Finding 1 detail: the transport banner is advisory This PR started as "BDS is NetherNet-only, so `crafty-l4-udp` is inert." **That premise is refuted.** The previous commit's recommendation (`transport=raknet`) was correct — honestly labelled *REASONED, NOT OBSERVED*, and now **OBSERVED**. What was wrong was reading an ERROR-level log line as a refusal without checking what the process did next. ### Evidence chain (all OBSERVED unless tagged) | # | Measurement | Command / source | |---|---|---| | 1 | `./bedrock_server` pid 47006 **still running** 5 min after printing the banner | `ps -eo pid,etime,args` in the `crafty` container | | 2 | **UDP 19132 + 19133 bound**: `/proc/net/udp` `00000000:4ABC`, `/proc/net/udp6` `…:4ABD`. The earlier **TCP** bind on `4ABC` is **gone** | `grep 4ABC /proc/net/udp /proc/net/udp6 /proc/net/tcp*` | | 3 | Both sockets owned by that pid | `/proc/47006/fd/25`, `fd/26` → those socket inodes | | 4 | Crafty's warning **changed kind**: last `Got timeout` `21:34:04,306`, first `too few fields` `21:34:09,854` (155 → 86 lines) | `/crafty/logs/commander.log` | | 5 | `Got timeout` fires only on `sock.recv` → `TimeoutError`; `too few fields` is reachable **only after a 0x1C Unconnected Pong arrives with valid magic** | `app/classes/remote_stats/raknet_ping.py`, read | | 6 | **End-to-end from off-cluster**: RakNet Unconnected Ping → `192.168.2.22:19132` returns a 139-byte `0x1c` pong, magic valid, ~12 ms: `MCPE;Dedicated Server;2193;1.26.52;0;10;…;Survival;1;19132;19133;0;0;0;` | hand-rolled RakNet ping from the toolbox on the host network | | 7 | Vendor doc **on the PVC** documents `raknet` as supported: *"the server listens directly on `server-port` (IPv4) and `server-portv6` (IPv6), and clients connect to that address and port"* | `bedrock_server_how_to.html`, shipped in the same archive | | 8 | RakNet still fully compiled in: `RakNetConnector`, `createUniqueRakPeer`, `RakPeerInterface`, `…/src-deps/raknet/raknet/RakNetSocket2.cpp` | `strings bedrock_server` | | 9 | Operator timeline: stop 21:32:32 → edit `server.properties` 21:33:31 → start 21:33:58 | `/crafty/logs/audit.log` | | 10 | `K3s-Bedrock` **still** `auto_start=0`, `crash_detection=0` | `crafty.sqlite`, opened read-only | So *"NetherNet is the only supported transport type"* is a **support** statement, not a **capability** one: Microsoft declines to support RakNet and ships it working. ### Why NetherNet could never have worked here — the finding the last pass missed The same vendor doc explains the previous pass's oddest measurement (19132 bound **TCP**): nethernet *"listens on a single dual-stack **TCP** socket on `server-port` for an HTTP-based signaling handshake."* And this repo exposes **no TCP 19132** — `crafty-l4-tcp` is 25565-25567 only, and `crafty-allow-ingress` allows `19132 UDP` and not `19132 TCP`. Unreachable at the Service layer *and* the policy layer. The server's own `packet-statistics.txt` for that run agrees: `Total Received: 0, 0B` over 2293 s. The 26 packets the last pass found on the `19132/UDP` policy map were players' RakNet pings hitting a port where nethernet had opened no UDP socket. Consistent, and now closed. ### Recommendation, ranked — with the manifest consequence of each | Rank | Option | Manifest consequence | Verdict | |---|---|---|---| | **1** | **Keep `transport=raknet`** | **none** — Service, policy and `containerPort`s were already correct, now proven end-to-end | **Take this.** Works today, vanilla parity, real Bedrock world. | | 2 | GeyserMC + Floodgate on the `homelab` Paper server | **none** (Geyser also binds `19132/udp`) but it **collides** with the live BDS — one must move | Reserve. Only if RakNet is really removed. Cost: Bedrock players join the **Java** world. Geyser's version support is **web/unmeasured**. | | 3 | Pin/downgrade BDS | none | Unnecessary — the *current* build speaks RakNet. Whether Crafty can pin a version is **UNVERIFIED**. | | 4 | PocketMine-MP / Nukkit | none | No — loses vanilla parity and world format. | | 5 | Accept NetherNet | 19132-19134/UDP goes inert; would need **TCP** 19132 added to `crafty-l4-tcp` **and** `crafty-allow-ingress`, plus `server-udp-ports` pinned and exposed | No — strictly *more* work than the option that already works, and WebRTC media behind a shared VIP + SNAT is unproven here. | **The 19132-19134/UDP exposure stays and is not inert.** The allowlist refusal is **orthogonal to transport** — it would refuse players identically under NetherNet. So "accept NetherNet" does **not** outrank the status quo: NetherNet is not what is working, RakNet is, and switching would *add* manifest work while admitting **not one extra player**. ### Changes — `platform/crafty/README.md` and `DECISIONS-LOG.md` only. No manifest touched. - Rewrote *"the transport trap"* as **two** traps: nethernet hides the UDP socket, then the ERROR banner frightens you into reverting the fix. Full evidence chain, vendor quotes, binary symbols, pong. - Documented that Crafty's perpetual `too few fields - missing values [None]` is the **success** signature: BDS 1.26.52's pong has 16 `;`-fields, Crafty 4.10.8's `pingpong_string_fields` knows 14 and wants an empty terminator, gets `'0'`. Cosmetic. The failure signature is the *other* message. - Added the ranked options table and an *"If RakNet is ever really removed"* migration section. - `enable-lan-visibility` upgraded to **OBSERVED** and a previous claim **corrected**: `19133` is *not* unreachable — it answers with the same `server_guid`, so Bedrock slot 1 is genuinely occupied. - **Game VIP corrected**: the README asserted `192.168.1.55`, which is the base/dev value. `clusters/prod/kustomization.yaml` patches `CRAFTY_L4_VIP` to `192.168.2.22` — the live `EXTERNAL-IP` on both L4 Services. - Noted `transport=raknet` is PVC state Flux cannot restore, and silently regresses to the vendor default on a Bedrock server recreate or reinstall. - `DECISIONS-LOG.md`: **appended** a new entry. The prior entry is **not** edited or deleted — it reasoned correctly and flagged its own uncertainty honestly; the new entry records that the observation vindicated it and that the misread was of the *server's reaction*. ### Gate: rc 3 — UNDETERMINED. NOT green. Not a PASS. `ENGINE=<shim> DEV_KUBECONFIG=/dev/null ./infra/devcontainer/dev.sh run bash -c './ci/gate.sh'` from the worktree; diff base `origin/main @ 20ea70d` → `HEAD @ c59a4b7`. | 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** — R-TEST-06 genuinely verified (see below) | | `gitea-settings` | **SKIPPED** — `SETTINGS_API_TOKEN` unset | | `conformance` (L2/L3) | **SKIPPED** — `CLUSTER_AVAILABLE != true` | **rc 3, because two stages did not run.** Nothing failed; nothing beyond the green rows is certified. Touched requirements `R-DNS-01 R-NET-01 R-PLAT-02 R-STATE-01` are **L2/L3 UNVERIFIED**. `spec-immutability` initially skipped spuriously: `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 that skip into a real **green**, so the one normally-tolerated skip is closed here rather than accepted. Separately, and not from the gate, the enforcement seam was measured directly (CLAUDE.md §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`. **A green tick here still proves only the L0/L1 offline subset** — and a skipped Gitea job reports `success` with "Has been skipped", so read the description. ### What CI reported on `d29a73c` (the prior head) (measured, descriptions read — not just the tick) context='gate / gate (pull_request)' status='success' description='Successful in 1m35s' context='gate / conformance (pull_request)' status='success' description='Has been skipped' Combined commit status: `state: success`. **Neither tick means the gate reached rc 0.** - `gate / conformance` is `if: false` in `.gitea/workflows/gate.yml` — it **never runs** and reports `success` with *"Has been skipped"*. This is the exact trap CLAUDE.md §2 names. - `gate / gate` — the one required context — genuinely ran (1m35s), but the workflow invokes `./ci/gate.sh || [ "$?" -eq 3 ]` (line 134). **CI tolerates exit 3 by declared design.** So its green means *"gate.sh exited 0 **or** 3"*. My local run exited **3**, and CI would report exactly this same green for it. The tick is not evidence of rc 0 and must not be read as one. ### Still open — NOT weakened by the success - **`auto_start=0` / `crash_detection=0` are still `0`** (re-measured 22:06, after the join). See operator action 1 — this is now the urgent item. - **The silent-reset exposure, now precisely characterised:** a **restart** preserves `transport=raknet` and `allowlist.json` (observed) but leaves the server down because `auto_start=0`; a **server recreate or reinstall** silently resets `server.properties` to `transport=nethernet` (nothing bound on 19132/udp) **and** `allowlist.json` to `[]` (everyone refused). Neither announces itself, and the panel shows the server healthy. Both files are PVC state Flux cannot restore. - **L2/L3 conformance is still uncertified** — `R-DNS-01 R-NET-01 R-PLAT-02 R-STATE-01` remain UNVERIFIED in CI. A player joining is not a conformance test. - **Consoles still cannot reach this**; LAN/Friends discovery still cannot work behind the VIP. - **Geyser remains entirely unmeasured**; BDS version pinnability remains UNVERIFIED. ### UNVERIFIED — read before acting - **The join itself is operator-reported, not watched by me.** Corroborated on disk by the server-written XUID (above), which is why it is listed as evidence rather than as unverified — but the distinction is kept deliberately. - **Only ONE simultaneous session is proven.** Whether a *second* concurrent Bedrock player evicts the first (masquerade port reuse / UDP conntrack behind the shared VIP) is **untested** — 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. - **The multi-player SNAT question is still open.** Whether a *second* simultaneous player evicts the first (masquerade port reuse / UDP conntrack behind a shared VIP) is untested. One ping is one flow. - **Geyser is entirely unmeasured here** — nothing installed or run. Its version support, jar names and config keys in the README are documentation-sourced. - **Whether Crafty can pin a BDS version** — UNVERIFIED, its installer was not read. - **Whether a future BDS really removes RakNet** — unknowable from here. The banner is a deprecation notice with an unknown expiry. - A web search on the error string was attempted and was **low quality** (results echoed the error back). **Nothing here rests on it** — all load-bearing evidence is local: `/proc/net/*`, `/proc/47006/fd/*`, `audit.log`, `commander.log`, `raknet_ping.py`, the shipped how-to, `strings bedrock_server`, and the pong. ### Read-only, as briefed No `kubectl apply/edit/patch/delete/scale/rollout`, no `flux reconcile`, **no write anywhere inside the container** — `server.properties` was read, never touched. Pod is still `crafty-64f7b6d78-pkpgp` (2/2, 15d, 0 restarts). All probes were reads plus outbound UDP pings from the toolbox. **Not merged.** ### Operator actions 1. **URGENT — `PATCH /api/v2/servers/c5837301-12b1-404e-b50e-70eafb37226b`** setting `auto_start` **and** `crash_detection` true, **and read them back**. Both are **still `0`**, re-measured at 22:06 *after* the successful join and after the operator's 22:02:28 "modified the server" action, which did not change them. This is the only thing between the working server and it silently vanishing at the next pod restart — and **merging `fix/crafty-oauth2-proxy-signin` is such a restart** (`strategy: Recreate`, RWO volume), which would stop **both** Minecraft servers. The failure mode is now worse than before: "worked, then vanished" rather than "never worked". 2. **Add a gamertag per additional player** — console `allowlist add <Gamertag>` (no restart needed), verify with `allowlist list`. 3. **Do not set `allow-list=false`** — the game port is un-gated by design, so the allowlist is the only join control (and console `allowlist off` does not persist to `server.properties`). 4. **Leave `transport=raknet` alone.** **Do not revert it because of the `TRANSPORT TYPE ERROR` banner, and do not chase the `too few fields` warning** — both appear on a fully working server. 5. **Before announcing to more than one player:** only **one** simultaneous session is proven. If a second player evicts the first, that is the SNAT/UDP-conntrack question and needs its own measured investigation. 6. **(superseded by 1) `auto_start`** `PATCH /api/v2/servers/c5837301-12b1-404e-b50e-70eafb37226b` setting `auto_start` and `crash_detection` true, **and read them back** (the create API drops them). Until then Bedrock does not survive a pod restart — and merging `fix/crafty-oauth2-proxy-signin` restarts the pod (`strategy: Recreate`), stopping **both** servers. 7. **Set `enable-lan-visibility=false`** on `K3s-Bedrock` if a second Bedrock server is ever wanted — it currently occupies **both** 19132 and 19133 (measured). 8. **Re-check after any Bedrock recreate or BDS update** — re-run the 5-step connect procedure; the allowlist resets to `[]` and: confirm `transport=raknet` survived, then re-ping. The vendor default is `nethernet` and the regression is silent. 9. Optional: wire `SETTINGS_API_TOKEN` (`infra/gitea/set-ci-token.sh`) to close the `gitea-settings` skip in future gate runs. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
gitea-admin added 1 commit 2026-09-30 02:23:08 +00:00
docs(crafty): Bedrock 19132 refuses nothing -- BDS ships transport=nethernet
gate / gate (pull_request) Successful in 1m35s
gate / conformance (pull_request) Has been skipped
07f03536b3
Reported as "users cannot connect to Bedrock on UDP 19132". Measured read-only
on prod; the platform delivered the packets and the server never opened the
socket.

Server `K3s-Bedrock` (c5837301-..., type=minecraft-bedrock, server_port=19132)
exists and `./bedrock_server` is alive, but /proc/net/udp and /proc/net/udp6 in
the crafty container hold NO socket on 19132 (0x4ABC) in either family -- port
19132 is bound as TCP (/proc/net/tcp6 ...:4ABC state 0A). Cause, from the
server's own server.properties: transport=nethernet, the value Microsoft's BDS
archive ships. Its inline docs: `Allowed values: "raknet" or "nethernet"`, and
against server-portv6, `This property is ignored when transport=nethernet`. NetherNet is the
WebRTC/Xbox-signalled transport and opens no RakNet UDP listener. Crafty does
not manage the property (no `transport` anywhere under /crafty/app/classes/) and
its only Bedrock probe is raknet_ping.py, hence commander.log spamming
"Got timeout while issuing bedrock ping to 127.0.0.1:19132" -- over loopback,
which indicts the server, not the network.

The path below it is sound and was measured as such: crafty-l4-udp live as
externalTrafficPolicy: Cluster with the VIP in status.loadBalancer, a non-empty
EndpointSlice, `cilium-dbg service list` showing 192.168.2.22:19132/UDP =>
10.0.0.130:19132/UDP (active) on BOTH announcing nodes, the datapath policy map
carrying `Allow Ingress reserved:world 19132/UDP` with 26 packets / 27660 bytes
already counted through it, pihole answering crafty-mc -> 192.168.2.22, and Flux
at main@sha1:20ea70d == the GitRepository artifact.

So this is not a platform or manifest defect. No manifest is touched: adding
ports or widening policy for this would be manufacturing work. The repo-side
defect is documentation -- the README told the operator that creating a server
on 19132 was sufficient.

README: add "Bedrock on UDP 19132 -- the transport trap" (the measurement chain,
and the raknet_ping warning as its signature) and "Bedrock realities that are NOT
platform defects" (no LAN/Friends discovery behind a VIP, consoles cannot enter
an address, enable-lan-visibility collides with slots 1-2, server-portv6 is not
exposed). Correct three stale claims: "the operator has not created any servers
yet" (two exist), the human test implying a started Bedrock server is reachable,
and the single-server Runtime state section (now a two-row table recording
K3s-Bedrock's auto_start=0 / crash_detection=0 -- the create-API trap, live
again, so it will not survive a pod restart).

Also measured, reported, and deliberately NOT changed: the shared VIP's two
leases sit on different nodes (crafty-l4-tcp on k3s-prod-3, crafty-l4-udp on
k3s-prod-1), so two nodes ARP-announce 192.168.2.22. Benign under `Cluster` --
both nodes carry the full active frontend set -- and the existing L0 check
l2-announced-vips-serve-from-any-node already forbids the `Local` that would make
it fatal. A "shared VIPs must co-locate" check would assert runtime lease
placement a static check cannot see.

Read-only throughout: no apply/edit/patch, no flux reconcile, no write inside
the container. Operator actions (set transport=raknet and verify the UDP bind;
PATCH auto_start; tell players to Add Server by hand) are in the DECISIONS-LOG
entry, along with what is UNVERIFIED -- chiefly that `raknet` fixes it is
reasoned from the shipped docs, not observed.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
gitea-admin added 1 commit 2026-09-30 02:50:24 +00:00
docs(crafty): 19132/udp IS bound and answering -- the ERROR banner was advisory
gate / gate (pull_request) Successful in 1m35s
gate / conformance (pull_request) Has been skipped
d29a73cad8
The prior entry's operator fix (transport=raknet) was labelled REASONED, NOT
OBSERVED. The operator applied it, BDS 1.26.52 printed
"================ TRANSPORT TYPE ERROR ===================
 NetherNet is the only supported transport type", and that was read as a refusal.

It was not a refusal. The process kept running and bound the RakNet sockets:
/proc/net/udp 00000000:4ABC and /proc/net/udp6 ...:4ABD, both owned by
bedrock_server pid 47006 (fd/25, fd/26). The earlier TCP bind on 4ABC is gone.
Crafty's raknet_ping warning changed kind at 21:34 -- last "Got timeout"
21:34:04,306, first "too few fields" 21:34:09,854 -- and per raknet_ping.py the
latter is reachable only after a 0x1C Unconnected Pong arrives with valid magic.

Proven end-to-end from off-cluster: a RakNet Unconnected Ping to the prod VIP
192.168.2.22:19132 returns a 139-byte 0x1c pong in ~12ms:
  MCPE;Dedicated Server;2193;1.26.52;0;10;...;Bedrock level;Survival;1;19132;19133;0;0;0;

The vendor's own shipped bedrock_server_how_to.html documents raknet as a
supported value ("the server listens directly on server-port"), and RakNet is
still compiled in (RakNetConnector, createUniqueRakPeer, RakNetSocket2.cpp).
"Only supported transport type" is a support statement, not a capability one.

Also explains why nethernet could never have worked here: it signals over TCP on
server-port, and this repo exposes no TCP 19132 -- crafty-l4-tcp is 25565-25567
and crafty-allow-ingress allows 19132 UDP only. packet-statistics.txt for that
run: Total Received: 0, 0B over 2293s.

Decision: keep transport=raknet, NO manifest change. The 19132-19134/UDP
exposure is correct and live, not inert. Geyser+Floodgate documented as the
reserve migration if RakNet is ever really removed (also zero manifest change).

Other corrections: the "too few fields" warning is the success signature, not a
failure; enable-lan-visibility upgraded to OBSERVED (19133 answers with the same
server_guid, so slot 1 is NOT free -- previous text said it was unreachable);
Game VIP corrected from 192.168.1.55 (the base/dev value) to the live prod
192.168.2.22; noted transport=raknet is PVC state that silently regresses to
nethernet on a server recreate. K3s-Bedrock auto_start=0/crash_detection=0
re-confirmed from crafty.sqlite and still outstanding.

Read-only throughout: no kubectl mutation, no flux reconcile, no write inside
the container. server.properties was read, never touched.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
gitea-admin changed title from docs(crafty): Bedrock 19132 refuses nothing — BDS ships transport=nethernet to docs(crafty): Bedrock 19132 IS bound and answering — the TRANSPORT TYPE ERROR banner is advisory 2026-09-30 02:51:40 +00:00
gitea-admin added 2 commits 2026-09-30 03:00:45 +00:00
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]>
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
cd2617e5b3
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]>
gitea-admin changed title from docs(crafty): Bedrock 19132 IS bound and answering — the TRANSPORT TYPE ERROR banner is advisory to docs(crafty): Bedrock 19132 is reachable — the wall is an empty allowlist, not the transport 2026-09-30 03:01:32 +00:00
gitea-admin added 1 commit 2026-09-30 03:10:26 +00:00
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
c59a4b76bb
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]>
gitea-admin changed title from docs(crafty): Bedrock 19132 is reachable — the wall is an empty allowlist, not the transport to docs(crafty): Bedrock on 19132 RESOLVED — a player has joined; the wall was an empty allowlist 2026-09-30 03:11:14 +00:00
jakman85 approved these changes 2026-09-30 03:19:00 +00:00
jakman85 merged commit cfbc596b8b into main 2026-09-30 03:19:04 +00:00
Sign in to join this conversation.
No Reviewers
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Home-Network/home-network-k3s-impl#23