-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Kyle Stemen
committed
Nov 13, 2023
1 parent
2992d6f
commit 4332d4a
Showing
16 changed files
with
2,178 additions
and
0 deletions.
There are no files selected for viewing
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
build |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
cmake_minimum_required(VERSION 3.21) | ||
project(vs_protocol | ||
VERSION 1.0 | ||
DESCRIPTION "Wireshark dissector the Vintage Story" | ||
) | ||
|
||
set(CMAKE_CXX_STANDARD 17) | ||
set(CMAKE_CXX_STANDARD_REQUIRED ON) | ||
|
||
find_package(Wireshark 4.0 CONFIG REQUIRED) | ||
find_package(PkgConfig REQUIRED) | ||
pkg_check_modules(ZSTD REQUIRED libzstd) | ||
|
||
SET(PLUGIN_DIR "${Wireshark_PLUGIN_INSTALL_DIR}" CACHE STRING | ||
"Directory to install the dissector plugin" | ||
) | ||
SET(PROTOBUF_DIR | ||
"${Wireshark_INSTALL_PREFIX}/share/wireshark/protobuf" | ||
CACHE STRING "Directory to install protobuf files into") | ||
|
||
# Turn on all warnings and set the warnings as errors. | ||
if(MSVC) | ||
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) | ||
add_compile_options(/W3 /WX) | ||
else() | ||
add_compile_options(-Wall -Werror) | ||
endif() | ||
|
||
# Declare the plugin that will be built, and list all of its cpp sources (do | ||
# not include headers here). | ||
add_library(vintage_story SHARED | ||
"client_id.cpp" | ||
"dissector.cpp" | ||
"server_id.cpp" | ||
) | ||
|
||
# Link the plugin against the Wireshark epan library. The PRIVATE keyword means | ||
# that other programs that link against the plugin will not also directly link | ||
# against epan. Since nothing will compile time link against the plugin, the | ||
# PRIVATE keyword basically has no effect for this library. | ||
target_link_libraries(vintage_story PRIVATE epan) | ||
|
||
# Include the zstd library. Wireshark will have builtin support for zstd | ||
# through the `tvb_child_uncompress_zstd` function in the 4.2 release, but it | ||
# is not in a stable release yet. | ||
target_link_libraries(vintage_story PRIVATE ${ZSTD_LIBRARIES}) | ||
target_include_directories(vintage_story PRIVATE ${ZSTD_INCLUDE_DIRS}) | ||
target_compile_options(vintage_story PRIVATE ${ZSTD_CFLAGS_OTHER}) | ||
|
||
# This is an epan plugin. So install into ${CMAKE_INSTALL_LIBDIR}/epan, so that | ||
# Wireshark loads it. If it were installed directly into the versioned plugins | ||
# directory (if "epan" was left off), then Wireshark would ignore it. | ||
install(TARGETS vintage_story LIBRARY DESTINATION "${PLUGIN_DIR}/epan") | ||
|
||
install(FILES vintage_story.proto DESTINATION "${PROTOBUF_DIR}") |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
## Example output | ||
|
||
![screenshot](https://raw.githubusercontent.com/bluelightning32/vs-protocol/main/doc/example1.png) | ||
|
||
## Supported features | ||
|
||
Currently only Linux is supported. | ||
|
||
Connections to multiplayer servers use TCP, which Wireshark can capture and decode. It does not work in single player mode. Single player mode uses a dummy connection that does not use TCP, such that Wireshark cannot see its traffic. | ||
|
||
Only the outer protobuf is parsed. Some protobufs have inner fields which are serialized with another format, such as protobuf again, TreeAttribute, or JSON. That second layer of serialization is not dissected. It just shows up as a byte array. | ||
|
||
## Installation directories | ||
|
||
Wireshark has some standard named folders. The actual location of these folders vary in different distros. The exact locations can be found in the folders tab of the About Wireshark dialog in the Help menu. Below is an example from Fedora 38. | ||
![screenshot](https://raw.githubusercontent.com/bluelightning32/vs-protocol/main/doc/wireshark_folders.png) | ||
|
||
The final files consist of a shared library plugin (`libvintage_story.so`) and a Protocol Buffer definition (`vintage_story.proto`). These files may either be installed in a global location for all users (requires root access), or in a directory for one user (does not require root access). | ||
|
||
* Global installation | ||
* Install `libvintage_story.so` in the "Global Plugins" folder (typically /usr/lib64/wireshark/plugins/4.0). | ||
* Install `vintage_story.proto` in the "Global configuration" folder (typically /usr/share/wireshark). | ||
* User installation | ||
* Install `libvintage_story.so` in the "Personal Plugins" folder (typically ~/.local/lib/wireshark/plugins/4.0). Create the folder if it does not exist yet. | ||
* Install `vintage_story.proto` in the "Global configuration" folder (typically ~/.config/wireshark). Create the folder if it does not exist yet. | ||
|
||
If it worked, the plugin will be listed on the Plugins tab of the About dialog. | ||
![screenshot](https://raw.githubusercontent.com/bluelightning32/vs-protocol/main/doc/wireshark_plugins.png) | ||
|
||
## Dissecting traffic on a non-default VS server port | ||
|
||
The plugin will automatically recognize TCP traffic to VS servers running on the default 42420 port. For servers running on other ports, first capture the data. Then select a packet from the server to the client (do not use a packet from the client to the server), then click 'Decode As...'. Double click the Current column, and select VintageStory in the drop down. Click Save then OK. | ||
![screenshot](https://raw.githubusercontent.com/bluelightning32/vs-protocol/main/doc/decode_as.png) | ||
|
||
## Decoding large packets | ||
|
||
At the network layer, large packets get fragmented into smaller packets. The vs-protocol dissector relies on Wireshark's built-in TCP dissector to reassemble those fragments so that they can be decoded as a full VS packet. For very large packets, such as the ServerAssets packet, those fragments often get reordered on the network. By default the TCP dissector does not handle reordered fragments. It is advised to turn on the 'Reassemble out-of-order segments' so that it does handle that, so that the vs-protocol dissector and decode those very large packets. | ||
|
||
The setting can be found in the Wireshark Preferences dialog, under the 'Protocols > TCP' tree. | ||
![screenshot](https://raw.githubusercontent.com/bluelightning32/vs-protocol/main/doc/tcp_preferences.png) | ||
|
||
Those very large packets add more Protobuf fields to the tree than Wireshark supports by default. This shows up as a warning 'Adding protobuf.field.number would put more than xxx items in the tree -- possible infinite loop' in the protocol tree. This limit can be increased through the gui.max\_tree\_items preference, but doing so does seem to make the GUI less responsive on the large packets. | ||
![screenshot](https://raw.githubusercontent.com/bluelightning32/vs-protocol/main/doc/max_tree_elements.png) | ||
|
||
## Building | ||
|
||
Building requires the following to be installed: | ||
* A C++ compiler | ||
* The Wireshark-dev package | ||
* The libzstd-devel package | ||
* CMake | ||
|
||
First create a build directory, typically as a subdirectory inside of the source checkout, and enter it. | ||
``` | ||
mkdir build | ||
cd build | ||
``` | ||
|
||
Run CMake to configure the plugin. With the default options, the plugin will be installed in the global location (requires root). | ||
``` | ||
cmake ../ | ||
``` | ||
|
||
Alternatively, options can be passed to CMake to install in the user directories, which does not require root. | ||
``` | ||
cmake ../ -DPLUGIN_DIR:STRING=~/.local/lib/wireshark/plugins/4.0 -DPROTOBUF_DIR:STRING=~/.config/wireshark/protobuf | ||
``` | ||
|
||
Finally build the dissector and install it. | ||
``` | ||
make install | ||
``` | ||
|
||
## Notes on dependencies | ||
|
||
Vintage Story uses ZStandard to compress large packets. So the dissector must be able to decompress ZStandard. Wireshark 4.2 has builtin support for zstd, but 4.2 is still labelled as a developer release. The stable version, 4.0, does not export a ZStandard function to plugins. So instead vs-protocol links directly against libzstd. | ||
|
||
Wireshark supports writing plugins in LUA, which is more portable than C/C++. However, even in the Wireshark development build, the vs-protocol plugin cannot be written in LUA, because Wireshark does not expose all of the compression functions in the LUA API ([issue 16451](https://gitlab.com/wireshark/wireshark/-/issues/16451)). Furthermore, Wireshark does not include any way for LUA to call arbitrary native APIs. Theoretically one could rewrite zstd in LUA, but that would be very difficult. | ||
|
||
This dissector is GPLv2 licensed, because all Wireshark plugins [must be](https://wiki.wireshark.org/Lua#beware-the-gpl) GPLv2 licensed, even LUA dissectors. Furthermore, the Vintage Story Protobuf file was contributed by Tyron with the understanding that it would be GPLv2 licensed. |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
#include "client_id.h" | ||
|
||
namespace vintage_story { | ||
|
||
const std::unordered_map<int, const char *> client_ids { | ||
{33, "LoginTokenQuery"}, | ||
{1, "PlayerIdentification"}, | ||
{2, "PingReply"}, | ||
{3, "BlockPlaceOrBreak"}, | ||
{4, "ChatLine"}, | ||
{7, "ActivateInventorySlot"}, | ||
{8, "MoveItemstack"}, | ||
{9, "FlipItemstacks"}, | ||
{10, "CreateItemstack"}, | ||
{11, "RequestJoin"}, | ||
{12, "SpecialKey"}, | ||
{13, "SelectedHotbarSlot"}, | ||
{14, "Leave"}, | ||
{15, "ServerQuery"}, | ||
|
||
{17, "EntityInteraction"}, | ||
{19, "PlayerPosition"}, | ||
{20, "RequestModeChange"}, | ||
{21, "MoveKeyChange"}, | ||
|
||
{22, "BlockEntityPacket"}, | ||
{31, "EntityPacket"}, | ||
{23, "CustomPacket"}, | ||
{25, "HandInteraction"}, | ||
{26, "ClientLoaded"}, | ||
{27, "SetToolMode"}, | ||
{28, "BlockDamage"}, | ||
{29, "ClientPlaying"}, | ||
{30, "InvOpenClose"}, | ||
{32, "RuntimeSetting"}, | ||
}; | ||
|
||
} // namespace vintage_story |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
#ifndef VINTAGE_STORY_CLIENT_ID_H__ | ||
#define VINTAGE_STORY_CLIENT_ID_H__ | ||
|
||
#include <unordered_map> | ||
|
||
namespace vintage_story { | ||
|
||
extern const std::unordered_map<int, const char *> client_ids; | ||
|
||
} // namespace vintage_story | ||
|
||
#endif // VINTAGE_STORY_CLIENT_ID_H__ |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
# This is a script to help convert the decompiled VS source code back into | ||
# protobufs. This script ended up being unnecessary for version 1, because | ||
# Tyron contributed the real protobuf file. The script is kept around in case | ||
# it helps in updating the Protobuf files on the next VS version update. | ||
|
||
import re | ||
import sys | ||
|
||
class_def = re.compile(r"\s*(public\s+)?class\s+Packet_(\S+)\s*(:.*)?$") | ||
field_def = re.compile(r"\s*(public\s+)?(Packet_)?([^=,]+)\s+(\S+);$") | ||
field_id_regex = re.compile(r"\s*(public\s+)?const int\s+(\S+)FieldID = (\S+);$") | ||
|
||
current_class = None | ||
fields = [] | ||
field_ids = {} | ||
|
||
def translate_type(field_type): | ||
if field_type == "byte[]": | ||
return "bytes" | ||
|
||
if field_type.endswith("[]"): | ||
return 'repeated ' + translate_type(field_type[:-2]) | ||
|
||
if field_type == "uint": | ||
return "uint32" | ||
if field_type == "ulong": | ||
return "uint64" | ||
if field_type == "int": | ||
return "int32" | ||
if field_type == "long": | ||
return "int64" | ||
return field_type | ||
|
||
def flush_class(): | ||
if current_class is None: | ||
return | ||
print('') | ||
print('message %s {' % current_class) | ||
for field_name, field_type in fields: | ||
if field_name not in field_ids: | ||
if field_name.endswith("Count"): | ||
continue | ||
if field_name.endswith("Length"): | ||
continue | ||
print('Missing key for field %s of type %s' % (field_name, field_type)) | ||
print(' %s %s = %s;' % (translate_type(field_type), field_name, field_ids[field_name])) | ||
print('}') | ||
fields.clear() | ||
field_ids.clear() | ||
|
||
print('syntax = "proto3";') | ||
print('package Packet;') | ||
|
||
for line in map(str.rstrip, sys.stdin): | ||
if match := re.fullmatch(class_def, line): | ||
flush_class() | ||
class_name = match.group(2) | ||
current_class = class_name | ||
elif match := re.fullmatch(field_def, line): | ||
field_type = match.group(3) | ||
field_name = match.group(4) | ||
if field_type == "return": | ||
continue | ||
fields.append((field_name, field_type)) | ||
elif match := re.fullmatch(field_id_regex, line): | ||
field_name = match.group(2) | ||
field_id = match.group(3) | ||
field_ids[field_name] = field_id |
Oops, something went wrong.