Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Kyle Stemen committed Nov 13, 2023
1 parent 2992d6f commit 4332d4a
Show file tree
Hide file tree
Showing 16 changed files with 2,178 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
build
55 changes: 55 additions & 0 deletions CMakeLists.txt
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}")
80 changes: 80 additions & 0 deletions README.md
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.
38 changes: 38 additions & 0 deletions client_id.cpp
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
12 changes: 12 additions & 0 deletions client_id.h
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__
68 changes: 68 additions & 0 deletions convert.py
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
Loading

0 comments on commit 4332d4a

Please sign in to comment.