gcpp/docs/ADDING_PACKET_PARSERS.md

214 lines
8.6 KiB
Markdown

# Adding New Packet Parsers
This guide explains how to implement parsers for new GCNet packet types.
## Architecture Overview
The packet parsing system uses several design patterns:
```
┌─────────────────────────────────────────────────────────────────┐
│ GCNetPacketProcessor │
│ ┌─────────────────────────────────────────────────────────────┐│
│ │ PacketParserFactory (Factory) ││
│ │ ┌────────────┐ ┌────────────┐ ┌──────────────────────────┐ ││
│ │ │ PingParser │ │ PongParser │ │ VerifyAccountRequestParser│ ││
│ │ └────────────┘ └────────────┘ └──────────────────────────┘ ││
│ │ Strategy Pattern ││
│ └─────────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ PacketContext │
│ • decrypted payload bytes │
│ • TCP segment (src/dst IP:port) │
│ • server port for direction detection │
└─────────────────────────────────────────────────────────────────┘
```
### Design Patterns Used
| Pattern | Class | Purpose |
|---------|-------|---------|
| **Strategy** | `PacketParser` interface | Each opcode has its own parsing strategy |
| **Factory** | `PacketParserFactory` | Creates the correct parser for each opcode |
| **Context** | `PacketContext` | Immutable context with payload + connection metadata |
| **Registry** | `GCNetOpcode` enum | Maps opcode numbers to metadata and direction hints |
| **Null Object** | `GenericPayloadParser` | Fallback for unimplemented opcodes |
## Step-by-Step: Adding a New Parser
### 1. Define the Opcode
Add the opcode to `GCNetOpcode.java`:
```java
// In packets.com.gcpp.GCNetOpcode
/**
* Example: Player join request.
* Structure: Player name (string), Character class (int), Level (short)
*/
ENU_PLAYER_JOIN_REQ(50, "ENU_PLAYER_JOIN_REQ",
"Player join request", Direction.CLIENT_TO_SERVER),
```
The `Direction` enum indicates expected traffic flow:
- `CLIENT_TO_SERVER` — Sent from client **to** server port (dstPort == serverPort)
- `SERVER_TO_CLIENT` — Sent from server **from** server port (srcPort == serverPort)
- `BIDIRECTIONAL` — Valid both ways (e.g., PING/PONG)
### 2. Create the Parser
Create a new class in `packets/parsers/`:
```java
package com.gcnet.decryptor.packets.parsers;
import packets.com.gcemu.gcpp.GCNetOpcode;
import packets.com.gcemu.gcpp.PacketContext;
import packets.com.gcemu.gcpp.PacketParser;
import java.nio.ByteBuffer;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Parser for ENU_PLAYER_JOIN_REQ packets (opcode 50).
*
* <h3>Packet Structure:</h3>
* <table border="1">
* <tr><th>Offset</th><th>Size</th><th>Type</th><th>Description</th></tr>
* <tr><td>0</td><td>2</td><td>ushort (BE)</td><td>Opcode (50)</td></tr>
* <tr><td>2</td><td>4</td><td>int (BE)</td><td>Content size</td></tr>
* <tr><td>6</td><td>1</td><td>bool</td><td>Compression flag</td></tr>
* <tr><td>7</td><td>4</td><td>int (LE)</td><td>Player name byte length</td></tr>
* <tr><td>11</td><td>var</td><td>string (UTF-16LE)</td><td>Player name</td></tr>
* <tr><td>var</td><td>4</td><td>int (LE)</td><td>Character class ID</td></tr>
* <tr><td>var</td><td>2</td><td>short (LE)</td><td>Character level</td></tr>
* </table>
*
* <p>Direction: {@link GCNetOpcode.Direction#CLIENT_TO_SERVER}</p>
*/
@PacketParser.PacketParserFor(opcode = 50)
public class PlayerJoinRequestParser implements PacketParser {
private static final int CONTENT_OFFSET = 7;
@Override
public ParseResult parse(PacketContext context) {
byte[] payload = context.decryptedPayload();
if (payload.length < CONTENT_OFFSET + 10) {
return ParseResult.empty(GCNetOpcode.ENU_PLAYER_JOIN_REQ, "Client → Server");
}
int offset = CONTENT_OFFSET;
Map<String, String> fields = new LinkedHashMap<>();
// Parse player name
ByteBuffer bb = ByteBuffer.wrap(payload, offset, 4)
.order(java.nio.ByteOrder.LITTLE_ENDIAN);
int nameLen = bb.getInt();
offset += 4;
if (offset + nameLen <= payload.length) {
String playerName = new String(payload, offset, nameLen, java.nio.charset.StandardCharsets.UTF_16LE);
fields.put("Player Name", playerName);
offset += nameLen;
}
// Parse character class
bb = ByteBuffer.wrap(payload, offset, 4)
.order(java.nio.ByteOrder.LITTLE_ENDIAN);
int charClass = bb.getInt();
fields.put("Character Class", String.valueOf(charClass));
offset += 4;
// Parse level
bb = ByteBuffer.wrap(payload, offset, 2)
.order(java.nio.ByteOrder.LITTLE_ENDIAN);
short level = bb.getShort();
fields.put("Level", String.valueOf(level));
return ParseResult.parsed(
GCNetOpcode.ENU_PLAYER_JOIN_REQ,
"Client → Server",
"ENU_PLAYER_JOIN_REQ { name: string_utf16_le, class_id: int32, level: int16 }",
fields,
formatContentHex(payload),
"player=\"" + fields.getOrDefault("Player Name", "?") + "\""
);
}
private String formatContentHex(byte[] data) {
var sb = new StringBuilder();
for (int i = 0; i < data.length; i++) {
if (i % 16 == 0 && i > 0) sb.append("\n");
sb.append(String.format("%02X ", data[i]));
}
return sb.toString().trim();
}
}
```
### 3. Register the Parser
Add registration in `PacketParserFactory.registerBuiltInParsers()`:
```java
private void registerBuiltInParsers() {
// ... existing registrations ...
// Player Management
registerParser(50, new PlayerJoinRequestParser());
registerParser(51, new PlayerJoinAckParser());
}
```
### 4. Direction Detection
The `PacketContext` automatically determines direction:
```java
public boolean isClientToServer() {
return segment.dstPort() == serverPort; // Destination is the server
}
public boolean isServerToClient() {
return segment.srcPort() == serverPort; // Source is the server
}
```
For a server on port 9501:
- `10.0.1.10:51094 → 10.0.1.10:9501`**Client → Server** (dstPort == 9501)
- `10.0.1.10:9501 → 10.0.1.10:51094`**Server → Client** (srcPort == 9501)
## File Structure
```
src/main/java/com/gcnet/decryptor/packets/
├── GCNetOpcode.java ← Opcode registry (add new opcodes here)
├── PacketContext.java ← Immutable context object
├── PacketParser.java ← Strategy interface + annotation
├── PacketParserFactory.java ← Factory (register new parsers here)
└── parsers/
├── GenericPayloadParser.java ← Fallback for unknown opcodes
├── KeyExchangeParser.java ← Opcode 1
├── PingParser.java ← Opcode 39
├── PongParser.java ← Opcode 40
├── VerifyAccountRequestParser.java ← Opcode 34
└── VerifyAccountAckParser.java ← Opcode 35
```
## Testing
Run the decryptor with your capture file:
```bash
java -jar target/gcnet-decryptor-1.0.0-jar-with-dependencies.jar capture.pcapng 9501
```
New parsers will automatically be invoked for their registered opcodes, producing structured field output instead of raw hex dumps.