214 lines
8.6 KiB
Markdown
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.
|