open-legacy-rpc 0.0.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- open_legacy_rpc-0.0.2/LICENSE +21 -0
- open_legacy_rpc-0.0.2/PKG-INFO +150 -0
- open_legacy_rpc-0.0.2/README.md +135 -0
- open_legacy_rpc-0.0.2/open_legacy_rpc.egg-info/PKG-INFO +150 -0
- open_legacy_rpc-0.0.2/open_legacy_rpc.egg-info/SOURCES.txt +39 -0
- open_legacy_rpc-0.0.2/open_legacy_rpc.egg-info/dependency_links.txt +1 -0
- open_legacy_rpc-0.0.2/open_legacy_rpc.egg-info/requires.txt +4 -0
- open_legacy_rpc-0.0.2/open_legacy_rpc.egg-info/top_level.txt +1 -0
- open_legacy_rpc-0.0.2/protocol/__init__.py +0 -0
- open_legacy_rpc-0.0.2/protocol/analyze_pcap.py +137 -0
- open_legacy_rpc-0.0.2/protocol/bitstream.py +229 -0
- open_legacy_rpc-0.0.2/protocol/diff_experiment.py +180 -0
- open_legacy_rpc-0.0.2/protocol/huffman.py +142 -0
- open_legacy_rpc-0.0.2/protocol/master_client.py +201 -0
- open_legacy_rpc-0.0.2/protocol/master_serialization.py +363 -0
- open_legacy_rpc-0.0.2/protocol/master_server.py +404 -0
- open_legacy_rpc-0.0.2/protocol/pcap_reader.py +147 -0
- open_legacy_rpc-0.0.2/protocol/raknet.py +260 -0
- open_legacy_rpc-0.0.2/protocol/raknet_connection.py +514 -0
- open_legacy_rpc-0.0.2/protocol/raknet_transport.py +514 -0
- open_legacy_rpc-0.0.2/protocol/unity_client.py +184 -0
- open_legacy_rpc-0.0.2/protocol/unity_protocol.py +1019 -0
- open_legacy_rpc-0.0.2/protocol/unity_rpc_decode.py +422 -0
- open_legacy_rpc-0.0.2/protocol/unity_server.py +345 -0
- open_legacy_rpc-0.0.2/pyproject.toml +37 -0
- open_legacy_rpc-0.0.2/setup.cfg +4 -0
- open_legacy_rpc-0.0.2/tests/test_bitstream.py +297 -0
- open_legacy_rpc-0.0.2/tests/test_cross_assembly_batch_index_dispatch.py +110 -0
- open_legacy_rpc-0.0.2/tests/test_cross_conformance.py +350 -0
- open_legacy_rpc-0.0.2/tests/test_decode_pcap.py +377 -0
- open_legacy_rpc-0.0.2/tests/test_decode_safety.py +159 -0
- open_legacy_rpc-0.0.2/tests/test_delta_state_sync.py +224 -0
- open_legacy_rpc-0.0.2/tests/test_exception_safety.py +72 -0
- open_legacy_rpc-0.0.2/tests/test_master_server.py +653 -0
- open_legacy_rpc-0.0.2/tests/test_network_groups.py +214 -0
- open_legacy_rpc-0.0.2/tests/test_python_integration.py +410 -0
- open_legacy_rpc-0.0.2/tests/test_raknet_transport.py +355 -0
- open_legacy_rpc-0.0.2/tests/test_unity_interop.py +84 -0
- open_legacy_rpc-0.0.2/tests/test_unity_interop_server.py +128 -0
- open_legacy_rpc-0.0.2/tests/test_unity_protocol.py +1088 -0
- open_legacy_rpc-0.0.2/tests/test_viewid_allocation.py +202 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-2026 Rose
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: open-legacy-rpc
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: Reverse-engineered wire format and implementation of Unity's legacy RPC networking (NetworkView / [RPC] / OnSerializeNetworkView)
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Repository, https://github.com/Rose-Vibe/open-legacy-rpc
|
|
7
|
+
Project-URL: Documentation, https://github.com/Rose-Vibe/open-legacy-rpc/tree/main/docs
|
|
8
|
+
Requires-Python: >=3.8
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: pytest; extra == "dev"
|
|
13
|
+
Requires-Dist: ruff~=0.14; extra == "dev"
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
# open-legacy-rpc
|
|
17
|
+
|
|
18
|
+
Reverse-engineered wire format spec and implementations (Python + TypeScript)
|
|
19
|
+
of Unity's legacy RPC networking stack (`NetworkView` / `[RPC]` /
|
|
20
|
+
`OnSerializeNetworkView`), last supported in Unity 2018.1.6f1. The transport
|
|
21
|
+
layer is RakNet 3.731; this project documents and implements **Unity's
|
|
22
|
+
undocumented framing on top of RakNet**.
|
|
23
|
+
|
|
24
|
+
Unity is a trademark of Unity Technologies. This project is independent,
|
|
25
|
+
clean-room reverse-engineered, and not affiliated with or endorsed by Unity
|
|
26
|
+
Technologies.
|
|
27
|
+
|
|
28
|
+
## Repository Structure
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
protocol/ Python reference implementation (client + server)
|
|
32
|
+
ts/ TypeScript production implementation (client + server)
|
|
33
|
+
experiments/ Unity projects for capture-based protocol analysis
|
|
34
|
+
docs/research/ Wire format specifications
|
|
35
|
+
docs/notes/ Lab notes and setup guides
|
|
36
|
+
docs/plans/ Sprint plans (active and archived)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Prerequisites
|
|
40
|
+
|
|
41
|
+
- **Python**: 3.10+
|
|
42
|
+
- **Node.js**: 18+ (TypeScript implementation)
|
|
43
|
+
|
|
44
|
+
## Quick Start
|
|
45
|
+
|
|
46
|
+
### Python
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
cd protocol
|
|
50
|
+
pip install -e . # or: pip install -r ../requirements-dev.txt
|
|
51
|
+
python -m pytest ../tests/
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### TypeScript
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
cd ts
|
|
58
|
+
npm install
|
|
59
|
+
npm test
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Wire Format Specs
|
|
63
|
+
|
|
64
|
+
- [Unity Legacy Networking Wire Format](docs/research/unity-legacy-networking-wire-format.md)
|
|
65
|
+
- [Master Server Wire Format](docs/research/master-server-wire-format.md)
|
|
66
|
+
- [Spec Status](docs/research/spec-status.md)
|
|
67
|
+
|
|
68
|
+
## Usage
|
|
69
|
+
|
|
70
|
+
### TypeScript Server
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
import { UnityServer, createUdpSocket } from 'open-legacy-rpc';
|
|
74
|
+
|
|
75
|
+
const socket = createUdpSocket();
|
|
76
|
+
const server = new UnityServer(socket, { port: 7777 });
|
|
77
|
+
|
|
78
|
+
server.onConnect((session) => {
|
|
79
|
+
console.log(`Client connected: ${session.id}`);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
server.onRpc('MyRpc', (session, ...params) => {
|
|
83
|
+
console.log('RPC received:', params);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
await server.start();
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Python Client
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from protocol.unity_client import UnityClient
|
|
93
|
+
|
|
94
|
+
client = UnityClient()
|
|
95
|
+
client.connect('127.0.0.1', 7777)
|
|
96
|
+
|
|
97
|
+
client.send_rpc('MyRpc', [42], view_id=1)
|
|
98
|
+
|
|
99
|
+
for msg in client.poll(timeout=1.0):
|
|
100
|
+
print(msg)
|
|
101
|
+
|
|
102
|
+
client.disconnect()
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## PCAP Decoder
|
|
106
|
+
|
|
107
|
+
Decode pcap/pcapng captures into structured JSON through the full protocol
|
|
108
|
+
stack (RakNet → Unity → game-specific):
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
python3 tools/decode_pcap.py capture.pcap # all defaults
|
|
112
|
+
python3 tools/decode_pcap.py capture.pcap --config config.json # with config
|
|
113
|
+
python3 tools/decode_pcap.py capture.pcap --compact # single-line JSON
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Config file (all fields optional):
|
|
117
|
+
|
|
118
|
+
```json
|
|
119
|
+
{
|
|
120
|
+
"serverPort": 7777,
|
|
121
|
+
"methodSignatures": { "RPC_TakeDamage": ["int", "float"] },
|
|
122
|
+
"viewNames": { "8192": "Player" }
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Output uses discriminated unions at each layer (`type` field). Game-specific
|
|
127
|
+
data that can't be decoded is marked `{ "type": "not_deserialized", "rawHex": "..." }`
|
|
128
|
+
for downstream post-processing.
|
|
129
|
+
|
|
130
|
+
## Network Namespace
|
|
131
|
+
|
|
132
|
+
Unity's RakNet crashes when the host has >10 IPv4 addresses. Use the
|
|
133
|
+
included scripts to create an isolated network namespace:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
sudo bash netns-setup.sh # create namespace
|
|
137
|
+
sudo ip netns exec unitynet ./Player.x86_64 ... # run Unity inside
|
|
138
|
+
tshark -i veth-unet-h -f 'udp' -w /tmp/unity.pcap # capture from host
|
|
139
|
+
sudo bash netns-teardown.sh # clean up
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Shell Scripts
|
|
143
|
+
|
|
144
|
+
| Script | Purpose |
|
|
145
|
+
|--------|---------|
|
|
146
|
+
| `run-experiment.sh` | Run a Unity experiment with traffic capture |
|
|
147
|
+
| `run-interop-tests.sh` | End-to-end interop tests: Python <-> Unity |
|
|
148
|
+
| `run-server-only.sh` | Start a Unity server, wait for external connections |
|
|
149
|
+
| `netns-setup.sh` | Create network namespace for Unity |
|
|
150
|
+
| `netns-teardown.sh` | Tear down network namespace |
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# open-legacy-rpc
|
|
2
|
+
|
|
3
|
+
Reverse-engineered wire format spec and implementations (Python + TypeScript)
|
|
4
|
+
of Unity's legacy RPC networking stack (`NetworkView` / `[RPC]` /
|
|
5
|
+
`OnSerializeNetworkView`), last supported in Unity 2018.1.6f1. The transport
|
|
6
|
+
layer is RakNet 3.731; this project documents and implements **Unity's
|
|
7
|
+
undocumented framing on top of RakNet**.
|
|
8
|
+
|
|
9
|
+
Unity is a trademark of Unity Technologies. This project is independent,
|
|
10
|
+
clean-room reverse-engineered, and not affiliated with or endorsed by Unity
|
|
11
|
+
Technologies.
|
|
12
|
+
|
|
13
|
+
## Repository Structure
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
protocol/ Python reference implementation (client + server)
|
|
17
|
+
ts/ TypeScript production implementation (client + server)
|
|
18
|
+
experiments/ Unity projects for capture-based protocol analysis
|
|
19
|
+
docs/research/ Wire format specifications
|
|
20
|
+
docs/notes/ Lab notes and setup guides
|
|
21
|
+
docs/plans/ Sprint plans (active and archived)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Prerequisites
|
|
25
|
+
|
|
26
|
+
- **Python**: 3.10+
|
|
27
|
+
- **Node.js**: 18+ (TypeScript implementation)
|
|
28
|
+
|
|
29
|
+
## Quick Start
|
|
30
|
+
|
|
31
|
+
### Python
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
cd protocol
|
|
35
|
+
pip install -e . # or: pip install -r ../requirements-dev.txt
|
|
36
|
+
python -m pytest ../tests/
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### TypeScript
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
cd ts
|
|
43
|
+
npm install
|
|
44
|
+
npm test
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Wire Format Specs
|
|
48
|
+
|
|
49
|
+
- [Unity Legacy Networking Wire Format](docs/research/unity-legacy-networking-wire-format.md)
|
|
50
|
+
- [Master Server Wire Format](docs/research/master-server-wire-format.md)
|
|
51
|
+
- [Spec Status](docs/research/spec-status.md)
|
|
52
|
+
|
|
53
|
+
## Usage
|
|
54
|
+
|
|
55
|
+
### TypeScript Server
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
import { UnityServer, createUdpSocket } from 'open-legacy-rpc';
|
|
59
|
+
|
|
60
|
+
const socket = createUdpSocket();
|
|
61
|
+
const server = new UnityServer(socket, { port: 7777 });
|
|
62
|
+
|
|
63
|
+
server.onConnect((session) => {
|
|
64
|
+
console.log(`Client connected: ${session.id}`);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
server.onRpc('MyRpc', (session, ...params) => {
|
|
68
|
+
console.log('RPC received:', params);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
await server.start();
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Python Client
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from protocol.unity_client import UnityClient
|
|
78
|
+
|
|
79
|
+
client = UnityClient()
|
|
80
|
+
client.connect('127.0.0.1', 7777)
|
|
81
|
+
|
|
82
|
+
client.send_rpc('MyRpc', [42], view_id=1)
|
|
83
|
+
|
|
84
|
+
for msg in client.poll(timeout=1.0):
|
|
85
|
+
print(msg)
|
|
86
|
+
|
|
87
|
+
client.disconnect()
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## PCAP Decoder
|
|
91
|
+
|
|
92
|
+
Decode pcap/pcapng captures into structured JSON through the full protocol
|
|
93
|
+
stack (RakNet → Unity → game-specific):
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
python3 tools/decode_pcap.py capture.pcap # all defaults
|
|
97
|
+
python3 tools/decode_pcap.py capture.pcap --config config.json # with config
|
|
98
|
+
python3 tools/decode_pcap.py capture.pcap --compact # single-line JSON
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Config file (all fields optional):
|
|
102
|
+
|
|
103
|
+
```json
|
|
104
|
+
{
|
|
105
|
+
"serverPort": 7777,
|
|
106
|
+
"methodSignatures": { "RPC_TakeDamage": ["int", "float"] },
|
|
107
|
+
"viewNames": { "8192": "Player" }
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Output uses discriminated unions at each layer (`type` field). Game-specific
|
|
112
|
+
data that can't be decoded is marked `{ "type": "not_deserialized", "rawHex": "..." }`
|
|
113
|
+
for downstream post-processing.
|
|
114
|
+
|
|
115
|
+
## Network Namespace
|
|
116
|
+
|
|
117
|
+
Unity's RakNet crashes when the host has >10 IPv4 addresses. Use the
|
|
118
|
+
included scripts to create an isolated network namespace:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
sudo bash netns-setup.sh # create namespace
|
|
122
|
+
sudo ip netns exec unitynet ./Player.x86_64 ... # run Unity inside
|
|
123
|
+
tshark -i veth-unet-h -f 'udp' -w /tmp/unity.pcap # capture from host
|
|
124
|
+
sudo bash netns-teardown.sh # clean up
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Shell Scripts
|
|
128
|
+
|
|
129
|
+
| Script | Purpose |
|
|
130
|
+
|--------|---------|
|
|
131
|
+
| `run-experiment.sh` | Run a Unity experiment with traffic capture |
|
|
132
|
+
| `run-interop-tests.sh` | End-to-end interop tests: Python <-> Unity |
|
|
133
|
+
| `run-server-only.sh` | Start a Unity server, wait for external connections |
|
|
134
|
+
| `netns-setup.sh` | Create network namespace for Unity |
|
|
135
|
+
| `netns-teardown.sh` | Tear down network namespace |
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: open-legacy-rpc
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: Reverse-engineered wire format and implementation of Unity's legacy RPC networking (NetworkView / [RPC] / OnSerializeNetworkView)
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Repository, https://github.com/Rose-Vibe/open-legacy-rpc
|
|
7
|
+
Project-URL: Documentation, https://github.com/Rose-Vibe/open-legacy-rpc/tree/main/docs
|
|
8
|
+
Requires-Python: >=3.8
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: pytest; extra == "dev"
|
|
13
|
+
Requires-Dist: ruff~=0.14; extra == "dev"
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
# open-legacy-rpc
|
|
17
|
+
|
|
18
|
+
Reverse-engineered wire format spec and implementations (Python + TypeScript)
|
|
19
|
+
of Unity's legacy RPC networking stack (`NetworkView` / `[RPC]` /
|
|
20
|
+
`OnSerializeNetworkView`), last supported in Unity 2018.1.6f1. The transport
|
|
21
|
+
layer is RakNet 3.731; this project documents and implements **Unity's
|
|
22
|
+
undocumented framing on top of RakNet**.
|
|
23
|
+
|
|
24
|
+
Unity is a trademark of Unity Technologies. This project is independent,
|
|
25
|
+
clean-room reverse-engineered, and not affiliated with or endorsed by Unity
|
|
26
|
+
Technologies.
|
|
27
|
+
|
|
28
|
+
## Repository Structure
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
protocol/ Python reference implementation (client + server)
|
|
32
|
+
ts/ TypeScript production implementation (client + server)
|
|
33
|
+
experiments/ Unity projects for capture-based protocol analysis
|
|
34
|
+
docs/research/ Wire format specifications
|
|
35
|
+
docs/notes/ Lab notes and setup guides
|
|
36
|
+
docs/plans/ Sprint plans (active and archived)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Prerequisites
|
|
40
|
+
|
|
41
|
+
- **Python**: 3.10+
|
|
42
|
+
- **Node.js**: 18+ (TypeScript implementation)
|
|
43
|
+
|
|
44
|
+
## Quick Start
|
|
45
|
+
|
|
46
|
+
### Python
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
cd protocol
|
|
50
|
+
pip install -e . # or: pip install -r ../requirements-dev.txt
|
|
51
|
+
python -m pytest ../tests/
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### TypeScript
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
cd ts
|
|
58
|
+
npm install
|
|
59
|
+
npm test
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Wire Format Specs
|
|
63
|
+
|
|
64
|
+
- [Unity Legacy Networking Wire Format](docs/research/unity-legacy-networking-wire-format.md)
|
|
65
|
+
- [Master Server Wire Format](docs/research/master-server-wire-format.md)
|
|
66
|
+
- [Spec Status](docs/research/spec-status.md)
|
|
67
|
+
|
|
68
|
+
## Usage
|
|
69
|
+
|
|
70
|
+
### TypeScript Server
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
import { UnityServer, createUdpSocket } from 'open-legacy-rpc';
|
|
74
|
+
|
|
75
|
+
const socket = createUdpSocket();
|
|
76
|
+
const server = new UnityServer(socket, { port: 7777 });
|
|
77
|
+
|
|
78
|
+
server.onConnect((session) => {
|
|
79
|
+
console.log(`Client connected: ${session.id}`);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
server.onRpc('MyRpc', (session, ...params) => {
|
|
83
|
+
console.log('RPC received:', params);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
await server.start();
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Python Client
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from protocol.unity_client import UnityClient
|
|
93
|
+
|
|
94
|
+
client = UnityClient()
|
|
95
|
+
client.connect('127.0.0.1', 7777)
|
|
96
|
+
|
|
97
|
+
client.send_rpc('MyRpc', [42], view_id=1)
|
|
98
|
+
|
|
99
|
+
for msg in client.poll(timeout=1.0):
|
|
100
|
+
print(msg)
|
|
101
|
+
|
|
102
|
+
client.disconnect()
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## PCAP Decoder
|
|
106
|
+
|
|
107
|
+
Decode pcap/pcapng captures into structured JSON through the full protocol
|
|
108
|
+
stack (RakNet → Unity → game-specific):
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
python3 tools/decode_pcap.py capture.pcap # all defaults
|
|
112
|
+
python3 tools/decode_pcap.py capture.pcap --config config.json # with config
|
|
113
|
+
python3 tools/decode_pcap.py capture.pcap --compact # single-line JSON
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Config file (all fields optional):
|
|
117
|
+
|
|
118
|
+
```json
|
|
119
|
+
{
|
|
120
|
+
"serverPort": 7777,
|
|
121
|
+
"methodSignatures": { "RPC_TakeDamage": ["int", "float"] },
|
|
122
|
+
"viewNames": { "8192": "Player" }
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Output uses discriminated unions at each layer (`type` field). Game-specific
|
|
127
|
+
data that can't be decoded is marked `{ "type": "not_deserialized", "rawHex": "..." }`
|
|
128
|
+
for downstream post-processing.
|
|
129
|
+
|
|
130
|
+
## Network Namespace
|
|
131
|
+
|
|
132
|
+
Unity's RakNet crashes when the host has >10 IPv4 addresses. Use the
|
|
133
|
+
included scripts to create an isolated network namespace:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
sudo bash netns-setup.sh # create namespace
|
|
137
|
+
sudo ip netns exec unitynet ./Player.x86_64 ... # run Unity inside
|
|
138
|
+
tshark -i veth-unet-h -f 'udp' -w /tmp/unity.pcap # capture from host
|
|
139
|
+
sudo bash netns-teardown.sh # clean up
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Shell Scripts
|
|
143
|
+
|
|
144
|
+
| Script | Purpose |
|
|
145
|
+
|--------|---------|
|
|
146
|
+
| `run-experiment.sh` | Run a Unity experiment with traffic capture |
|
|
147
|
+
| `run-interop-tests.sh` | End-to-end interop tests: Python <-> Unity |
|
|
148
|
+
| `run-server-only.sh` | Start a Unity server, wait for external connections |
|
|
149
|
+
| `netns-setup.sh` | Create network namespace for Unity |
|
|
150
|
+
| `netns-teardown.sh` | Tear down network namespace |
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
open_legacy_rpc.egg-info/PKG-INFO
|
|
5
|
+
open_legacy_rpc.egg-info/SOURCES.txt
|
|
6
|
+
open_legacy_rpc.egg-info/dependency_links.txt
|
|
7
|
+
open_legacy_rpc.egg-info/requires.txt
|
|
8
|
+
open_legacy_rpc.egg-info/top_level.txt
|
|
9
|
+
protocol/__init__.py
|
|
10
|
+
protocol/analyze_pcap.py
|
|
11
|
+
protocol/bitstream.py
|
|
12
|
+
protocol/diff_experiment.py
|
|
13
|
+
protocol/huffman.py
|
|
14
|
+
protocol/master_client.py
|
|
15
|
+
protocol/master_serialization.py
|
|
16
|
+
protocol/master_server.py
|
|
17
|
+
protocol/pcap_reader.py
|
|
18
|
+
protocol/raknet.py
|
|
19
|
+
protocol/raknet_connection.py
|
|
20
|
+
protocol/raknet_transport.py
|
|
21
|
+
protocol/unity_client.py
|
|
22
|
+
protocol/unity_protocol.py
|
|
23
|
+
protocol/unity_rpc_decode.py
|
|
24
|
+
protocol/unity_server.py
|
|
25
|
+
tests/test_bitstream.py
|
|
26
|
+
tests/test_cross_assembly_batch_index_dispatch.py
|
|
27
|
+
tests/test_cross_conformance.py
|
|
28
|
+
tests/test_decode_pcap.py
|
|
29
|
+
tests/test_decode_safety.py
|
|
30
|
+
tests/test_delta_state_sync.py
|
|
31
|
+
tests/test_exception_safety.py
|
|
32
|
+
tests/test_master_server.py
|
|
33
|
+
tests/test_network_groups.py
|
|
34
|
+
tests/test_python_integration.py
|
|
35
|
+
tests/test_raknet_transport.py
|
|
36
|
+
tests/test_unity_interop.py
|
|
37
|
+
tests/test_unity_interop_server.py
|
|
38
|
+
tests/test_unity_protocol.py
|
|
39
|
+
tests/test_viewid_allocation.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
protocol
|
|
File without changes
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Analyze pcap files from Unity legacy networking experiments.
|
|
4
|
+
Dumps UDP payloads with hex + ASCII, identifies RakNet framing,
|
|
5
|
+
and highlights potential Unity-layer messages.
|
|
6
|
+
"""
|
|
7
|
+
import struct
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
from .pcap_reader import read_pcap
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def hexdump(data, prefix=' '):
|
|
14
|
+
"""Hex + ASCII dump."""
|
|
15
|
+
lines = []
|
|
16
|
+
for i in range(0, len(data), 16):
|
|
17
|
+
chunk = data[i:i+16]
|
|
18
|
+
hex_part = ' '.join(f'{b:02x}' for b in chunk)
|
|
19
|
+
ascii_part = ''.join(chr(b) if 32 <= b < 127 else '.' for b in chunk)
|
|
20
|
+
lines.append(f'{prefix}{i:04x} {hex_part:<48s} {ascii_part}')
|
|
21
|
+
return '\n'.join(lines)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# RakNet offline message magic
|
|
25
|
+
OFFLINE_MAGIC = bytes([
|
|
26
|
+
0x00, 0xFF, 0xFF, 0x00, 0xFE, 0xFE, 0xFE, 0xFE,
|
|
27
|
+
0xFD, 0xFD, 0xFD, 0xFD, 0x12, 0x34, 0x56, 0x78
|
|
28
|
+
])
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def classify_packet(payload):
|
|
32
|
+
"""Classify a UDP payload into RakNet packet types."""
|
|
33
|
+
if not payload:
|
|
34
|
+
return 'empty', {}
|
|
35
|
+
|
|
36
|
+
first = payload[0]
|
|
37
|
+
|
|
38
|
+
# Offline messages
|
|
39
|
+
if first == 0x05 and len(payload) > 17 and payload[1:17] == OFFLINE_MAGIC:
|
|
40
|
+
return 'open_connection_request', {'protocol_version': payload[17] if len(payload) > 17 else None}
|
|
41
|
+
if first == 0x06 and len(payload) > 17 and payload[1:17] == OFFLINE_MAGIC:
|
|
42
|
+
return 'open_connection_reply', {}
|
|
43
|
+
if first == 0x09 and len(payload) > 17:
|
|
44
|
+
return 'open_connection_request_2', {}
|
|
45
|
+
|
|
46
|
+
# Connected datagrams: check bitflags
|
|
47
|
+
# Bit 7 (MSB): is_ack
|
|
48
|
+
is_ack = (first >> 7) & 1
|
|
49
|
+
is_nak = (first >> 6) & 1 if not is_ack else 0
|
|
50
|
+
|
|
51
|
+
if is_ack:
|
|
52
|
+
return 'ack', {}
|
|
53
|
+
if is_nak:
|
|
54
|
+
return 'nak', {}
|
|
55
|
+
|
|
56
|
+
# Data datagram - try to parse encapsulated messages
|
|
57
|
+
return 'data', {'raw': payload}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def find_known_patterns(payload, context=''):
|
|
61
|
+
"""Look for known byte patterns in the payload."""
|
|
62
|
+
findings = []
|
|
63
|
+
|
|
64
|
+
# Look for float values we sent
|
|
65
|
+
for i in range(len(payload) - 3):
|
|
66
|
+
chunk = payload[i:i+4]
|
|
67
|
+
|
|
68
|
+
# Check for known int values (little-endian)
|
|
69
|
+
try:
|
|
70
|
+
val_le = struct.unpack('<i', chunk)[0]
|
|
71
|
+
val_be = struct.unpack('>i', chunk)[0]
|
|
72
|
+
if val_le in (0, 1, 42, -1, 2147483647, 10, 20, 30, 40, 99):
|
|
73
|
+
findings.append((i, f'int32_le={val_le}'))
|
|
74
|
+
if val_be in (0, 1, 42, -1, 2147483647, 10, 20, 30, 40, 99) and val_be != val_le:
|
|
75
|
+
findings.append((i, f'int32_be={val_be}'))
|
|
76
|
+
except struct.error:
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
# Check for known float values (little-endian)
|
|
80
|
+
try:
|
|
81
|
+
fval_le = struct.unpack('<f', chunk)[0]
|
|
82
|
+
fval_be = struct.unpack('>f', chunk)[0]
|
|
83
|
+
for known in [1.0, 3.14159, -1.0, 2.0, 3.0, -100.5, 0.001, 999.999, 3.14, 45.0, 90.0, 180.0]:
|
|
84
|
+
if abs(fval_le - known) < 0.001:
|
|
85
|
+
findings.append((i, f'float_le={fval_le:.6f}'))
|
|
86
|
+
if abs(fval_be - known) < 0.001 and abs(fval_be - fval_le) > 0.001:
|
|
87
|
+
findings.append((i, f'float_be={fval_be:.6f}'))
|
|
88
|
+
except struct.error:
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
# Look for known strings
|
|
92
|
+
for s in [b'Hello', b'Hello World', b'hello', b'A']:
|
|
93
|
+
idx = payload.find(s)
|
|
94
|
+
if idx >= 0:
|
|
95
|
+
findings.append((idx, f'string="{s.decode()}"'))
|
|
96
|
+
|
|
97
|
+
# Look for known byte patterns
|
|
98
|
+
idx = payload.find(b'\xDE\xAD\xBE\xEF')
|
|
99
|
+
if idx >= 0:
|
|
100
|
+
findings.append((idx, 'bytes=DEADBEEF'))
|
|
101
|
+
|
|
102
|
+
return findings
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def analyze_pcap(path):
|
|
106
|
+
"""Full analysis of a pcap file."""
|
|
107
|
+
print(f'\n{"="*70}')
|
|
108
|
+
print(f'Analyzing: {path}')
|
|
109
|
+
print(f'{"="*70}\n')
|
|
110
|
+
|
|
111
|
+
server_port = 7777
|
|
112
|
+
packets = list(read_pcap(path))
|
|
113
|
+
|
|
114
|
+
for i, (ts, src_port, dst_port, payload) in enumerate(packets):
|
|
115
|
+
direction = 'C→S' if dst_port == server_port else 'S→C'
|
|
116
|
+
ptype, info = classify_packet(payload)
|
|
117
|
+
|
|
118
|
+
print(f'Packet {i+1}: {direction} ({src_port}→{dst_port}) {len(payload)}B [{ptype}]')
|
|
119
|
+
|
|
120
|
+
# Show hex dump
|
|
121
|
+
print(hexdump(payload))
|
|
122
|
+
|
|
123
|
+
# Show any found patterns
|
|
124
|
+
findings = find_known_patterns(payload)
|
|
125
|
+
if findings:
|
|
126
|
+
print(f' ** Found: {", ".join(f"@{off}: {desc}" for off, desc in findings)}')
|
|
127
|
+
|
|
128
|
+
print()
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
if __name__ == '__main__':
|
|
132
|
+
if len(sys.argv) < 2:
|
|
133
|
+
print(f'Usage: {sys.argv[0]} <pcap-file> [pcap-file ...]')
|
|
134
|
+
sys.exit(1)
|
|
135
|
+
|
|
136
|
+
for path in sys.argv[1:]:
|
|
137
|
+
analyze_pcap(path)
|