Skip to content

Add cross-version compatibility harness (T0 + T1) - #190

Closed
jhead wants to merge 30 commits into
masterfrom
worktree-compat-harness
Closed

Add cross-version compatibility harness (T0 + T1)#190
jhead wants to merge 30 commits into
masterfrom
worktree-compat-harness

Conversation

@jhead

@jhead jhead commented Jul 26, 2026

Copy link
Copy Markdown
Owner

Validates phantom against 51 Minecraft protocol versions, fully automated in CI.
Design and rationale: test/compat/DESIGN.md.

Two tiers, split by testing philosophy

T0 — white box, ~0.3s, no network. Replays real captured pong bytes from all
51 bedrock-protocol versions, plus hand-authored shape fixtures (10/12/13/14/15
fields, hostile content, malformed frames), through the parser and rewriter. Adds
a fuzz target for the pong parser, which eats untrusted network bytes with no
length or magic validation today.

T1 — black box, ~40s. Drives a real phantom subprocess over real UDP against
a scriptable fake upstream, and for sampled versions a real bedrock-protocol
server and client. Subprocess rather than in-process because serverID is a
package global, so "two instances must advertise different IDs" is not observable
from inside one process.

The tier boundary is enforced, not conventional: T1 may not import
internal/proto, internal/proxy or internal/clientmap, and
TestBlackBoxRuleHolds checks that with go list -deps.

Eight invariants XFAIL today

These are the open protocol bugs already in TODO.md, now measured against real
wire bytes rather than asserted. The headline: every one of the 51 captured
versions sends 13 pong fields, and phantom drops the 13th.

The registry (internal/corpus/data/known_failures.json) fails the build if a
registered case starts passing, so fixing a bug forces deleting its entry in
the same change. CI is green on day one and the registry cannot rot.

CI

t0 runs on an OS matrix; t1 is sharded four ways. The sharding is for
isolation as much as speed: phantom binds UDP :19132 unconditionally with
SO_REUSEPORT, so two concurrent instances on one host silently load-balance
each other's datagrams. One shard per runner gives each its own network stack.

A nightly job sweeps every version, fuzzes for 10m, and flags version drift when
Mojang ships a release we don't cover.

Two unavoidable changes to phantom itself

  • go 1.12go 1.21go:embed needs ≥1.16, native fuzzing ≥1.18.
  • Fixed the unkeyed struct literal in cmd/phantom.go, since CI runs go vet
    and it failed on it.

Not yet verified

The full connect-session tests have never executed: bedrock-protocol's
native RakNet addon crashes on darwin/arm64 (trace/BPT trap), where this was
developed. They use a control connection straight to the upstream, so they skip
rather than falsely blame phantom — but this PR's CI run is their first real
execution.

🤖 Generated with Claude Code

jhead and others added 30 commits July 25, 2026 17:26
PowerShell treats bare -6 as a number, so the flag never reaches the process
(#124). Prefer -ipv6 and keep -6 as a legacy alias for other shells.

Co-authored-by: Cursor <cursoragent@cursor.com>
LAN pings were refreshing read deadlines while Write consumed ICMP errors, so the server never got marked offline and warnings spammed (#79).

Co-authored-by: Cursor <cursoragent@cursor.com>
When -server targets this host's :19132, SO_REUSEADDR can deliver DialUDP traffic back to the ping listener; ignore those sources so each echo does not open another socket until EMFILE.

Co-authored-by: Cursor <cursoragent@cursor.com>
Geyser MOTDs omit Port4/Port6, so consoles never learned phantom's
proxy port and the Friends/LAN entry failed to show reliably.

Co-authored-by: Cursor <cursoragent@cursor.com>
SO_REUSEADDR/PORT cannot share Bedrock discovery across processes, so a second
phantom instance stayed idle. Accept multiple -server values in one process and
reply for each upstream from a single exclusive discovery listener.

Co-authored-by: Cursor <cursoragent@cursor.com>
arm8 is aarch64, so 32-bit Pi OS users who pick it for "ARMv8" hit
Illegal instruction / Exec format error — clarify uname -m mapping and
add an arm64 alias.

Co-authored-by: Cursor <cursoragent@cursor.com>
A 1472-byte receive buffer truncated larger datagrams (often with err=nil), corrupting full-MTU pack transfer traffic. Use a full-size UDP buffer and fail closed on truncation.

Co-authored-by: Cursor <cursoragent@cursor.com>
Validates phantom against 51 Minecraft protocol versions, fully automated
in CI. Design in test/compat/DESIGN.md.

Two tiers, split by testing philosophy and enforced by imports:

T0 (white box, ~0.3s, no network) replays real captured pong bytes from
all 51 bedrock-protocol versions plus hand-authored shape fixtures
through the parser and rewriter, asserting field-level invariants. Adds
a fuzz target for the pong parser, which eats untrusted network bytes
with no length or magic validation.

T1 (black box, ~40s) drives a real phantom subprocess over real UDP
against a scriptable fake upstream and, for sampled versions, a real
bedrock-protocol server and client. It may not import phantom's
implementation packages; TestBlackBoxRuleHolds enforces that with
go list -deps rather than trusting convention.

Subprocess rather than in-process because serverID is a package global,
so "two instances must advertise different ids" is not observable from
inside one process.

CI shards T1 four ways for isolation as much as speed: phantom binds
UDP :19132 unconditionally with SO_REUSEPORT, so two concurrent
instances on one host silently load-balance each other's datagrams.
One shard per runner gives each its own network stack.

Eight invariants XFAIL today. These are the open protocol bugs in
TODO.md, now measured against real wire bytes rather than asserted:
every real server sends 13 pong fields and phantom drops the 13th. The
registry fails the build if a registered case starts PASSING, so fixing
a bug forces deleting its entry in the same change.

Also: bump go 1.12 -> 1.21 (go:embed needs >=1.16, fuzzing >=1.18) and
fix the unkeyed struct literal that made go vet fail.

Not verified: the full connect-session tests never execute on
darwin/arm64, where bedrock-protocol's native RakNet addon crashes.
They use a control connection to the upstream so they skip rather than
falsely blame phantom, but their first real run will be on Linux CI.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
One flaky i/o timeout was marking the whole proxy offline, so LAN clients got MaxPlayers=0 OfflinePong unless another peer's traffic cleared the flag (#104). Require sustained timeouts, and give Offline pongs placeholder ports so rewrite injects the bind port.

Co-authored-by: Cursor <cursoragent@cursor.com>
Unconnected pings no longer reuse gameplay DialUDP sessions, which could stop answering after disconnect while console re-pings kept the stale entry alive.

Co-authored-by: Cursor <cursoragent@cursor.com>
iSH needs phantom-linux-x86; SO_REUSEPORT fails with invalid argument so fall back to net.ListenPacket.

Co-authored-by: Cursor <cursoragent@cursor.com>
Termux runs standard linux/arm* builds; GOOS=android is unnecessary and needs the NDK. Clarify $HOME/noexec and uname -m mapping.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Combine multi-server discovery hub with prior offline, recv-buffer,
discovery-ping, self-echo, and -ipv6 fixes.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
macos-latest is now macos-26, whose dyld rejects Mach-O binaries with no
LC_UUID load command. Go's internal linker only began emitting one in
1.26 (golang/go#69987, golang/go#78012), so pinning go.mod's 1.21
minimum made every macOS test binary abort before any phantom code ran:

  dyld: missing LC_UUID load command in .../proxy.test
  signal: abort trap

This is a toolchain/OS interaction, not a phantom bug - internal/util,
which predates this harness, failed identically.

go.mod's directive stays at the true language minimum (embed needs
>=1.16, fuzzing >=1.18) and is still exercised by the `unit` job on
Linux, where LC_UUID is irrelevant. Only the OS matrix needs a current
toolchain.

Also records in DESIGN.md that the connect-session tests, previously
never executed anywhere, now run and pass on Linux CI: a real
bedrock-protocol client completes the full RakNet handshake and login
through phantom for every sampled version.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Removes the implementation-history and verification-status sections and
the per-dimension breakdown, and drops the date and revision markers.
The result describes the testing strategy as it stands rather than how
it came to be, so it does not need updating every time a test changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The T0 and T1 labels carried no meaning to a reader who had not already
read the design document. The tests are unit tests and end-to-end tests,
so they are now named that way throughout: known-failure ids, code
comments, Makefile targets, the version matrix and the CI job names.

CI jobs are now vet, unit, e2e and cross-compile. The job formerly
called unit, which runs gofmt/vet/race, is now vet.

Also tightens DESIGN.md prose.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The file listing duplicated the tree and went stale on every rename. The
Node scripts paragraph it carried moves into the end-to-end section,
where it is about design rather than location.

Also runs CI on pushes to dev.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Brings in dev's protocol work and drops the XFAIL registry, so every
invariant now asserts plainly and failures surface as failures.

Test updates required by dev's deliberate behavior changes:

- serverID moved from a package global to a per-instance ProxyServer
  field, so the rewrite tests construct it directly.
- The rewrite now always advertises phantom's bind port, injecting
  Port4/Port6 even when the upstream omitted them. The old test asserted
  the opposite, which was correct before the Geyser fix.
- cmd/phantom.go conflict resolved in favour of dev's multi-server main,
  which already used keyed struct fields.

Known red, to be fixed separately.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@jhead jhead closed this Jul 26, 2026
@jhead
jhead deleted the worktree-compat-harness branch July 26, 2026 02:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant