Repository navigation
Add rmi package: JRMP wire-protocol parser - #18
Merged
Merged
Conversation
A read-only parser for the JRMP Stream-protocol (0x4B) covering the full message surface: handshake, acknowledge, Call, ReturnData, Ping, PingAck, DgcAck. All five java.rmi.registry.Registry stub methods (bind, list, lookup, rebind, unbind) are semantically decoded; non-Registry Remote calls fall back to the raw serialization tree. Two entry points, one unified parseTransmission: - FromBytes(data): buffered input; sentinel-based arg loop (tolerant of non-Registry calls and ReturnData because the input eventually EOFs). - FromStream(r io.Reader): live connections (net.Conn etc.); derives each message's byte boundary from protocol framing alone, so it never blocks after finishing a frame. Registry calls only — non-Registry and Return return explicit errors directing the caller to FromBytes. Supporting changes: - serz.NewObjectStreamFromStream lets the embedded serialization parse share a commons.Stream byte cursor with its caller, so bytes peeked into one layer don't strand at a buffer handoff. - rmi CLI subcommand mirrors dump (-f file / -B base64). - Output is wireshark-dissector style: @serialization walks the stream once with inline semantic labels on each TCContent; the leading TC_BLOCKDATA is decomposed in place into ObjID + op + hash. - _tools/rmi-capture: Go-free socat tee-proxy + Java Driver for regenerating testcases/rmi fixtures (jdk17 / jdk8 subdirs). - 80+ unit and integration tests, including JDK 17 / JDK 8 real-capture parity and a streaming io.Pipe test with 5s timeout guard. - CLAUDE.md documents architecture, scope boundaries, and the "don't refactor back to serz.FromBytes" warning for the embedded stream. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Owner
Author
Code reviewFound 1 issue:
Lines 69 to 73 in 248852e 🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
- rmi/call.go, rmi/return.go: accept leading TC_BLOCKDATA longer than the fixed header, since non-Registry remotes with primitive params and methods with primitive return types legally append those raw bytes to the header block. Previously rejected as malformed. - rmi/rmi_test.go: cover both cases. - rmi/registry.go: reformat doc comment to Go 1.19+ gofmt style. - CLAUDE.md: point to the real buffered-mode sentinel loop in readCallArgs / readReturn instead of a nonexistent function.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a new
rmipackage that parses the JRMP (Java RMI) wire protocol end-to-end — built on top of the existingserzJava-serialization parser, with two parsing modes (buffered byte inputs and live TCP streams), wireshark-dissector-style output, and integration tests against realrmiregistrycaptures from both JDK 17 and JDK 8.Scope
Protocol coverage
MsgCall(0x50): full Java-serialization-embedded call framing; all fivejava.rmi.registry.Registrystub methods (bind/list/lookup/rebind/unbind) semantically decodedMsgReturnData(0x51): normal / exceptional / void payload variantsMsgPing/MsgPingAck/MsgDgcAck(0x52 / 0x53 / 0x54)ObjID == REGISTRY_IDANDmethodHash == RegistryInterfaceHashmust both match before a Call is treated as RegistryEntry points
Both delegate to one
parseTransmission(stream, streaming bool):rmi.FromBytes(data []byte).binfile,io.ReadAll-ed slice)rmi.FromStream(r io.Reader)net.Conn/ piperegistryArgCount(op))The split is deliberate: on a live TCP reader the sentinel would block forever after the last arg because the next-frame flag hasn't arrived yet; exact-count reading sidesteps this by consuming precisely the bytes the Call owns.
Output format (wireshark-dissector style)
Every byte gets a semantic label, printed exactly once. The leading
TC_BLOCKDATAis decomposed in place intoObjID+UID+@Operation+@MethodHash(each with its own hex slice). SubsequentTCContentargs carry inline labels likeTC_STRING - 0x74 (Registry.lookup arg 0: "name"). A compact@Decodedsummary at the top references complex args (bind's Remote stub etc.) by handler, so the stub tree is rendered once inside@Serialization, not duplicated.Implementation highlights
serz.NewObjectStreamFromStream(serz/buffer.go): 3-line constructor that wraps an existing*commons.Stream. This is what lets the rmi parser share a single byte cursor between outer JRMP framing and inner serz parsing — without it, peeking into either layer would strand bytes in the wrong buffer.operation=-1 + method hashformat. Real captures provedsun.rmi.registry.RegistryImpl_Stubuses legacyoperation=op_index + interface_hash(the five methods share oneRegistryInterfaceHash = 0x44154DC9D4E63BDF). Caught by integration tests (closed-loop bug in hand-crafted fixtures).SYSTEM:colon-parsing quirk: the capture script escapesTCP\:localhost\:1099because socat 1.8 splits theSYSTEM:argument on:. Documented in_tools/rmi-capture/README.md.commons.Stream.Seekforward-seek footgun discovered during development.Files
New package
rmi/(core parser):parser.go—FromBytes/FromStream/parseTransmission/Transmission.ToStringcall.go—CallMessage+readCall(mode-aware)return.go—ReturnMessage+readReturnmessage.go— message dispatcherhandshake.go—Handshake/Acknowledge/readModifiedUTFdgc.go—Ping/PingAck/DgcAckobjid.go—ObjID/UIDvalue typesregistry.go— method decoders +registryArgCountprinter.go— dissector-style rendering helpersmodel.go— protocol constantsTests:
rmi_test.go— 21 hand-crafted unit testsintegration_test.go— 14 integration tests × 2 JDKs (27 subtests, viaforEachJDK)streaming_test.go— 9 streaming tests includingio.Pipedelivery + 5-second blocking-guard timeoutCapture tooling (under
_tools/rmi-capture/, auto-ignored by Go toolchain):capture.sh— orchestratesrmiregistry+socattee-proxy + per-op JVMDriver.java— one-op-per-JVM RMI clientREADME.md— regeneration instructions, socat 1.8 gotcha documentedFixtures under
testcases/rmi/{jdk17,jdk8}/— 20 real captures (5 ops × 2 directions × 2 JDKs).Other changes:
main.go— newrmiCLI subcommand mirroringdump's-f/-Bflagsserz/buffer.go—NewObjectStreamFromStreamconstructorCLAUDE.md— architecture guide (new file).gitignore— ignore*.classartifactsTest plan
go test ./...— all 5 packages green (rmi has 80 subtests)go test -race ./...— greengolangci-lint run --no-config ./rmi/...— 0 issuesgo run main.go rmi -f testcases/rmi/jdk17/lookup-c2s.binproduces the dissector-style dumpio.Pipetest proves streaming parser doesn't block on live-reader semanticsserzysoserial byte-exact round-trip tests🤖 Generated with Claude Code