Skip to content

Commit

Permalink
Enable building on Windows
Browse files Browse the repository at this point in the history
  • Loading branch information
Kyle Stemen committed Nov 16, 2023
1 parent 4332d4a commit 4ea0ec0
Show file tree
Hide file tree
Showing 3 changed files with 140 additions and 68 deletions.
33 changes: 27 additions & 6 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,15 +1,27 @@
cmake_minimum_required(VERSION 3.21)
project(vs_protocol
VERSION 1.0
VERSION 1.0.1
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)
find_package(PkgConfig)
if(PkgConfig_FOUND)
pkg_check_modules(ZSTD REQUIRED libzstd)
else()
find_package(zstd CONFIG REQUIRED)
message(WARNING "test ${zstd_FOUND}")
message(WARNING "test2 ${ZSTD_INCLUDE_DIRS}")
message(WARNING "test2 ${zstd_INCLUDE_DIRS}")
get_property(dirs DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY INCLUDE_DIRECTORIES)
message(WARNING "test3 ${dirs}")
foreach(dir ${dirs})
message(STATUS "dir='${dir}'")
endforeach()
endif()

SET(PLUGIN_DIR "${Wireshark_PLUGIN_INSTALL_DIR}" CACHE STRING
"Directory to install the dissector plugin"
Expand Down Expand Up @@ -43,9 +55,18 @@ 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})
if(PkgConfig_FOUND)
# libzstd was found with the pkgconfig program (typically true on Linux).
# pkgconfig sets ZSTD_LIBRARIES, ZSTD_INCLUDE_DIRS, and ZSTD_CFLAGS_OTHER.
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})
else()
# libzstd was found through its cmake package (typically true on Windows).
# Linking against the cmake library name will automatically add the include
# directories to the build path.
target_link_libraries(vintage_story PRIVATE zstd::libzstd_shared)
endif()

# 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
Expand Down
44 changes: 37 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

## Supported features

Currently only Linux is supported.
Currently only Linux and Windows are 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.

Expand All @@ -15,14 +15,14 @@ Only the outer protobuf is parsed. Some protobufs have inner fields which are se
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).
The final files consist of a shared library plugin (`libvintage_story.so` on Linux or `vintage_story.dll` on Windows) and a Protocol Buffer definition (`vintage_story.proto`). These files may either be installed in a global location for all users (requires root/admin 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).
* Install `libvintage_story.so` (Linux) or `vintage_story.dll` (Windows) in the epan subfolder of the "Global Plugins" folder (typically /usr/lib64/wireshark/plugins/4.0/epan on Linux or "C:\Program Files\Wireshark\plugins\4.0\epan" on Windows).
* Install `vintage_story.proto` in the protobuf subfolder "Global configuration" folder (typically /usr/share/wireshark/protobuf on Linux or "C:\Program Files\Wireshark\protobuf" on Windows).
* 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.
* Install `libvintage_story.so` (Linux) or `vintage_story.dll` (Windows) in the "Personal Plugins" folder (typically ~/.local/lib/wireshark/plugins/4.0 on Linux or "C:\Users\username\AppData\Roaming\Wireshark\plugins\4.0" on Windows). Create the folder if it does not exist yet.
* Install `vintage_story.proto` in the protobuf subfolder of the "Global configuration" folder (typically ~/.config/wireshark/protobuf on Linux or "C:\Users\username\AppData\Roaming\Wireshark\protobuf" on Windows). 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)
Expand All @@ -42,7 +42,7 @@ The setting can be found in the Wireshark Preferences dialog, under the 'Protoco
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 on Linux

Building requires the following to be installed:
* A C++ compiler
Expand Down Expand Up @@ -71,6 +71,36 @@ Finally build the dissector and install it.
make install
```

## Building on Windows

Before the dissector can be built on Windows, Wireshark must be built from source to obtain the lib and header files. Those files are not available through any of the Wireshark precompiled installers. The official [Wireshark build directions](https://www.wireshark.org/docs/wsdg_html_chunked/ChSetupWindows) have all of the details, but the major points are summarized below. Note that it is not necessary to install the Qt dependency called out in the official directions. However, winflexbison3 does need to be installed in order to build Wireshark.

Install Visual Studio. The free community edition is sufficient. Within the Visual Studio installer, make sure the install cmake option is selected.

Download and unpack the Wireshark source code.

Start "x64 Native Tools Command Prompt for VS 2022" from the start menu. Create a temporary directory for Wireshark to download more build dependencies into ("development" is used below). The `-DBUILD_wireshark=off` option tells Wireshark to skip building the GUI, because the GUI is not necesary for building the plugin. This way Qt does not need to be installed to build the rest of Wireshark.
```
mkdir %USERPROFILE%\development
set WIRESHARK_BASE_DIR=%USERPROFILE%\development
mkdir build
cd build
cmake -G "Visual Studio 17 2022" -A x64 ..\ -DBUILD_wireshark=off
cmake --build . --prefix %USERPROFILE%\development\wireshark_install --config Release
cmake --install . --prefix %USERPROFILE%\development\wireshark_install --config Release
```

Download the vs-protocol source code and go to that directory inside the native tools command prompt. The `-DCMAKE_PREFIX_PATH` option tells CMake how to find dependencies. It needs to point to both the libraries build by Wireshark, and to the zstd dependency that was downloaded during the Wireshark build.
```
mkdir build
cd build
cmake ..\ -DCMAKE_PREFIX_PATH=%USERPROFILE%\development\wireshark_install;%USERPROFILE%\development\wireshark-win64-libs-4.0\zstd-1.5.2-1-win64ws\installed\x64-windows
cmake --build . --config Release
cmake --install . --config Release --prefix .
```

The files (`vintage_story.dll` and `vintage_story.proto`) are installed in the wrong location. Copy them to the correct locations. As described in the earlier installation directories section.

## 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.
Expand Down
131 changes: 76 additions & 55 deletions dissector.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include <algorithm> // for std::max
#include <iostream>
#include <iterator> // for std::size
#include <limits> // for std::numeric_limits
#include <sstream> // for std::ostringstream
#include <vector>

Expand Down Expand Up @@ -38,11 +39,11 @@ void SetInfoColumn(packet_info *pinfo,
col_add_str(pinfo->cinfo, COL_INFO, joined.c_str());
}

long DecodeVarInt(tvbuff_t *payload, size_t &offset) {
long DecodeVarInt(tvbuff_t *payload, gint &offset) {
long val = 0;
bool more = true;
int shift = 0;
while (offset < tvb_captured_length(payload) && more) {
while (tvb_captured_length_remaining(payload, offset) > 0 && more) {
unsigned char read = tvb_get_guint8(payload, offset);
val |= (read & 0x7F) << shift;
more = read & 0x80;
Expand All @@ -52,10 +53,16 @@ long DecodeVarInt(tvbuff_t *payload, size_t &offset) {
return val;
}

size_t GetProtoBufEnd(tvbuff_t *payload) {
size_t offset = 0;
while (offset < tvb_captured_length(payload)) {
size_t prev_offset = offset;
gint GetProtoBufEnd(tvbuff_t *payload) {
if (tvb_captured_length(payload) >
static_cast<guint>(std::numeric_limits<gint>::max())) {
std::cerr << "Captured packet size, " << tvb_captured_length(payload)
<< ", is beyond gint range." << std::endl;
return 0;
}
gint offset = 0;
while (tvb_captured_length_remaining(payload, offset) > 0) {
gint prev_offset = offset;
long tag = DecodeVarInt(payload, offset);
if (tag == 0) {
offset = prev_offset;
Expand All @@ -73,13 +80,13 @@ size_t GetProtoBufEnd(tvbuff_t *payload) {
offset += DecodeVarInt(payload, offset);
}
}
return std::min(offset, static_cast<size_t>(tvb_captured_length(payload)));
return std::min(offset, static_cast<gint>(tvb_captured_length(payload)));
}

int FindVarInt(tvbuff_t *payload, int find_tag, bool &found) {
found = false;
size_t offset = 0;
while (offset < tvb_captured_length(payload)) {
gint offset = 0;
while (tvb_captured_length_remaining(payload, offset) > 0) {
long tag = DecodeVarInt(payload, offset);
if (tag == 0) {
return -1;
Expand Down Expand Up @@ -113,7 +120,7 @@ std::string DecodePacket(bool is_server, tvbuff_t *payload,
// The serializer pads short messages with nulls to a length that's a
// multiple of 16. The protobuf dissector prints errors if they are
// passed to it. So use some heuristics to find the end of the message.
size_t payload_end = GetProtoBufEnd(payload);
gint payload_end = GetProtoBufEnd(payload);
tvbuff_t *unpadded = tvb_new_subset_length(payload, 0, payload_end);
call_dissector_with_data(
protobuf_handle, unpadded, pinfo, tree,
Expand All @@ -140,7 +147,7 @@ std::string DecodePacket(bool is_server, tvbuff_t *payload,
out << "id=" << id;
}
} else {
size_t offset = 0;
gint offset = 0;
long first_tag = DecodeVarInt(payload, offset);
if (first_tag == 10) {
out << "ServerIdentification";
Expand All @@ -153,7 +160,7 @@ std::string DecodePacket(bool is_server, tvbuff_t *payload,
// The client serializer pads short messages with nulls up to length 16.
// The protobuf dissector prints errors if they are passed to it. So use
// some heuristics to find the end of the message.
size_t payload_end = GetProtoBufEnd(payload);
gint payload_end = GetProtoBufEnd(payload);
tvbuff_t *unpadded = tvb_new_subset_length(payload, 0, payload_end);
call_dissector_with_data(
protobuf_handle, unpadded, pinfo, tree,
Expand All @@ -179,7 +186,7 @@ std::string DecodePacket(bool is_server, tvbuff_t *payload,
out << "id=" << id;
}
} else {
size_t offset = 0;
gint offset = 0;
long first_tag = DecodeVarInt(payload, offset);
if (first_tag == 18) {
out << "ClientIdentification";
Expand All @@ -191,7 +198,7 @@ std::string DecodePacket(bool is_server, tvbuff_t *payload,
return out.str();
}

int Dissect(tvbuff_t *buffer, packet_info *pinfo, proto_tree *tree,
gint Dissect(tvbuff_t *buffer, packet_info *pinfo, proto_tree *tree,
void* /*data*/) {
if (tvb_captured_length(buffer) < tvb_reported_length(buffer)) {
// Don't attempt to decode packets that were truncated because the snapshot
Expand All @@ -212,9 +219,9 @@ int Dissect(tvbuff_t *buffer, packet_info *pinfo, proto_tree *tree,
bool is_server = (pinfo->srcport == pinfo->match_uint);

std::vector<std::string> packet_names;
size_t offset = 0;
while (offset < tvb_captured_length(buffer)) {
int remaining = tvb_reported_length_remaining(buffer, offset);
gint offset = 0;
while (tvb_captured_length_remaining(buffer, offset) > 0) {
gint remaining = tvb_reported_length_remaining(buffer, offset);
if (remaining < 4) {
// The packet_length field is 4 bytes. If there are fewer than 4 bytes
// remaining, that means the packet was truncated. Setting
Expand Down Expand Up @@ -293,10 +300,17 @@ int Dissect(tvbuff_t *buffer, packet_info *pinfo, proto_tree *tree,
size_t final_decomp_size = ZSTD_decompress(decomp_buffer, decomp_size,
compressed_payload,
packet_length);
if (final_decomp_size > std::numeric_limits<gint>::max()) {
std::cerr << "Decompressed size, " << final_decomp_size
<< ", is too large to fit in a Wireshark buffer."
<< std::endl;
return 0;
}

// Wrap the decompressed data in a tvbuff_t.
payload = tvb_new_child_real_data(buffer, decomp_buffer,
final_decomp_size, final_decomp_size);
static_cast<gint>(final_decomp_size),
static_cast<gint>(final_decomp_size));
add_new_data_source(pinfo, payload, "Decompressed payload");
}
packet_names.push_back(DecodePacket(is_server, payload, pinfo, subtree));
Expand All @@ -316,62 +330,67 @@ void ProtoRegiser() {
static hf_register_info fields[] = {
{
&hf_compressed, {
.name = "compressed",
.abbrev = "vs.compressed",
.type = FT_BOOLEAN,
.display = 32,
.strings = nullptr,
.bitmask = 0x80000000,
.blurb = nullptr,
// Visual Studio complains about mixing designated initializers and
// non-designated initializers (from HFILL). So avoid using designated
// initializers.
/*.name =*/ "compressed",
/*.abbrev =*/ "vs.compressed",
/*.type =*/ FT_BOOLEAN,
/*.display =*/ 32,
/*.strings =*/ nullptr,
/*.bitmask =*/ 0x80000000,
/*.blurb =*/ nullptr,
HFILL
}
},
{
&hf_packet_length, {
.name = "packet length",
.abbrev = "vs.length",
.type = FT_UINT32,
.display = BASE_DEC,
.strings = nullptr,
.bitmask = 0x7fffffff,
.blurb = nullptr,
/*.name =*/ "packet length",
/*.abbrev =*/ "vs.length",
/*.type =*/ FT_UINT32,
/*.display =*/ BASE_DEC,
/*.strings =*/ nullptr,
/*.bitmask =*/ 0x7fffffff,
/*.blurb =*/ nullptr,
HFILL
}
},
{
&hf_compressed_payload, {
.name = "compressed payload",
.abbrev = "vs.compressed_payload",
.type = FT_BYTES,
.display = BASE_NONE,
.strings = nullptr,
.bitmask = 0,
.blurb = nullptr,
/*.name =*/ "compressed payload",
/*.abbrev =*/ "vs.compressed_payload",
/*.type =*/ FT_BYTES,
/*.display =*/ BASE_NONE,
/*.strings =*/ nullptr,
/*.bitmask =*/ 0,
/*.blurb =*/ nullptr,
HFILL
}
},
{
&hf_padding, {
.name = "0 padding",
.abbrev = "vs.zero_padding",
.type = FT_BYTES,
.display = BASE_NONE,
.strings = nullptr,
.bitmask = 0,
.blurb = nullptr,
/*.name =*/ "0 padding",
/*.abbrev =*/ "vs.zero_padding",
/*.type =*/ FT_BYTES,
/*.display =*/ BASE_NONE,
/*.strings =*/ nullptr,
/*.bitmask =*/ 0,
/*.blurb =*/ nullptr,
HFILL
}
},
};
proto_register_field_array(proto_id, fields, std::size(fields));
proto_register_field_array(proto_id, fields,
static_cast<int>(std::size(fields)));

// This does not need to be static. `proto_register_subtree_array` only uses
// it as an output array.
int * const subtree_expansions[] = {
&protocol_ett,
&compressed_payload_ett,
};
proto_register_subtree_array(subtree_expansions, std::size(subtree_expansions));
proto_register_subtree_array(subtree_expansions,
static_cast<int>(std::size(subtree_expansions)));
}

void ProtoRegHandoff() {
Expand All @@ -381,16 +400,20 @@ void ProtoRegHandoff() {

} // namespace

extern "C" {
#if defined(_WIN32) || defined __CYGWIN__
#define EXPORT extern "C" __declspec(dllexport)
#else
#define EXPORT extern "C" __attribute__((visibility("default")))
#endif

// These must be declared extern so that the symbols are exported in the .so
// library, so that Wireshark can find them. Otherwise, the plugin will fail to
// load with a 'has no plugin_version symbol' error.
extern const char plugin_version[] = "1.0.0";
extern const int plugin_want_major = WIRESHARK_VERSION_MAJOR;
extern const int plugin_want_minor = WIRESHARK_VERSION_MINOR;
EXPORT const char plugin_version[] = "1.0.1";
EXPORT const int plugin_want_major = WIRESHARK_VERSION_MAJOR;
EXPORT const int plugin_want_minor = WIRESHARK_VERSION_MINOR;

void plugin_register()
EXPORT void plugin_register()
{
// This must be static. Wireshark references it after this function returns.
static proto_plugin plug;
Expand All @@ -400,6 +423,4 @@ void plugin_register()
proto_register_plugin(&plug);
}

} // end extern "C"

} // namespace vintage_story

0 comments on commit 4ea0ec0

Please sign in to comment.