diff --git a/Dockerfile b/Dockerfile index 0b80944..a2f3ec7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,7 +18,8 @@ RUN apt-get update && apt-get install -y \ libasound2-dev:i386 \ libconfig++-dev:i386 \ libx11-dev:i386 \ - libcurl4-gnutls-dev:i386 + libcurl4-gnutls-dev:i386 \ + libsodium-dev:i386 # Copy files for building to container RUN mkdir /pumptools diff --git a/Package.mk b/Package.mk index 9ac4cfe..fd18f84 100644 --- a/Package.mk +++ b/Package.mk @@ -170,6 +170,7 @@ $(zipdir)/profile-tools.zip: \ $(zipdir)/pumpnet.zip: \ $(builddir)/bin/pumpnet-client \ + $(builddir)/bin/pumpnet-prinet-proxy \ | $(zipdir)/ $(V)echo ... $@ $(V)zip -j $@ $^ diff --git a/cmake/src/main/pumpnet/CMakeLists.txt b/cmake/src/main/pumpnet/CMakeLists.txt index 54a82bc..d4bc06c 100644 --- a/cmake/src/main/pumpnet/CMakeLists.txt +++ b/cmake/src/main/pumpnet/CMakeLists.txt @@ -1,2 +1,3 @@ add_subdirectory(client) -add_subdirectory(lib) \ No newline at end of file +add_subdirectory(lib) +add_subdirectory(prinet-proxy) \ No newline at end of file diff --git a/cmake/src/main/pumpnet/prinet-proxy/CMakeLists.txt b/cmake/src/main/pumpnet/prinet-proxy/CMakeLists.txt new file mode 100644 index 0000000..e48fd2b --- /dev/null +++ b/cmake/src/main/pumpnet/prinet-proxy/CMakeLists.txt @@ -0,0 +1,16 @@ +project(pumpnet-prinet-proxy) +message(STATUS "Project " ${PROJECT_NAME}) + +set(SRC ${PT_ROOT_MAIN}/pumpnet/prinet-proxy) + +add_resources(PRI_PRIVATE_KEY ${SRC} prime.private.key) +add_resources(PRI_PUBLIC_KEY ${SRC} prime.public.key) + +set(SOURCE_FILES + ${SRC}/main.c + ${SRC}/options.c + ${SRC}/packet.c) + +add_executable(${PROJECT_NAME} ${SOURCE_FILES} ${PRI_PRIVATE_KEY} ${PRI_PUBLIC_KEY}) + +target_link_libraries(${PROJECT_NAME} pumpnet-lib sec-prinet util) \ No newline at end of file diff --git a/src/main/pumpnet/prinet-proxy/main.c b/src/main/pumpnet/prinet-proxy/main.c new file mode 100644 index 0000000..d5989d8 --- /dev/null +++ b/src/main/pumpnet/prinet-proxy/main.c @@ -0,0 +1,290 @@ +#include +#include +#include + +#include "pumpnet/lib/prinet.h" +#include "pumpnet/prinet-proxy/options.h" +#include "pumpnet/prinet-proxy/packet.h" +#include "sec/prinet/prinet.h" + +#include "util/iobuf.h" +#include "util/log.h" +#include "util/mem.h" +#include "util/sock-tcp.h" + +static const uint32_t NUM_CONNECTIONS = 10; +static const size_t PUMPNET_MAX_RESP_SIZE = 1024 * 1024; + +static atomic_int _source_sock; + +/* Compiled binary data from data folder. Symbols are defined by compiler */ +extern const uint8_t _binary_prime_private_key_start[]; +extern const uint8_t _binary_prime_private_key_end[]; +extern const uint8_t _binary_prime_public_key_start[]; +extern const uint8_t _binary_prime_public_key_end[]; + +static bool _transform_data_request(const struct pumpnet_prinet_proxy_packet* packet, struct util_iobuf* dec_data) +{ + size_t enc_data_len = pumpnet_prinet_proxy_packet_get_data_len(packet); + + size_t dec_data_len = sec_prinet_decrypt( + packet->nounce, + sizeof(packet->nounce), + packet->data, + enc_data_len, + dec_data->bytes); + + if (dec_data_len != enc_data_len) { + log_error("Decrypting data failed"); + return false; + } + + dec_data->pos = dec_data_len; + + return true; +} + +static bool _transform_data_response(const struct pumpnet_prinet_proxy_packet* req_packet, const struct util_iobuf* dec_data, struct pumpnet_prinet_proxy_packet* resp_packet) +{ + size_t enc_data_len = sec_prinet_encrypt( + req_packet->nounce, + sizeof(req_packet->nounce), + dec_data->bytes, + dec_data->pos, + resp_packet->data); + + if (enc_data_len < 0) { + log_error("Encrypting data failed"); + return false; + } + + memcpy(resp_packet->nounce, req_packet->nounce, sizeof(resp_packet->nounce)); + + resp_packet->length = pumpnet_prinet_proxy_packet_get_header_len() + enc_data_len; + + return true; +} + +static bool _recv_packet_length(int handle, uint32_t* length) +{ + uint32_t packet_length; + + while (true) { + ssize_t read = util_sock_tcp_recv_block(handle, &packet_length, sizeof(uint32_t)); + + if (read == 0) { + log_error("Unexpected remote close and no data"); + return NULL; + } + + if (read != sizeof(uint32_t)) { + return false; + } + + break; + } + + *length = packet_length; + + return true; +} + +static struct pumpnet_prinet_proxy_packet* _recv_request_source(int handle) +{ + uint32_t packet_length; + + if (!_recv_packet_length(handle, &packet_length)) { + log_error("Receiving length field for request from source failed"); + return NULL; + } + + log_debug("Received length source request (%X): %d", handle, packet_length); + + struct pumpnet_prinet_proxy_packet* packet = util_xmalloc(packet_length); + packet->length = packet_length; + + size_t data_len = pumpnet_prinet_proxy_packet_get_data_len(packet); + + size_t read_pos = 0; + + while (true) { + size_t remaining_size = packet->length - sizeof(uint32_t) - read_pos; + + ssize_t read = util_sock_tcp_recv_block( + handle, + ((uint8_t*) packet) + sizeof(uint32_t) + read_pos, + remaining_size); + + if (read == 0) { + log_error("Unexpected remote close and no data"); + return NULL; + } + + if (read == -1) { + log_error("Receiving length field for request from source failed: %d", read); + return NULL; + } + + read_pos += read; + + if (read_pos >= data_len) { + break; + } + } + + return packet; +} + +static bool _send_response_source(int handle, const struct pumpnet_prinet_proxy_packet* packet) +{ + log_debug("Sending source response (%X): %d", handle, packet->length); + + if (util_sock_tcp_send_block(handle, packet, packet->length) != packet->length) { + log_error("Sending response, len %d, to source failed", packet->length); + return false; + } else { + return true; + } +} + +static void _sigint_handler(int sig) +{ + log_info("SIGINT, exiting"); + + util_sock_tcp_close(_source_sock); + + sec_prinet_finit(); + + pumpnet_lib_prinet_shutdown(); + + exit(EXIT_SUCCESS); +} + +int main(int argc, char** argv) +{ + util_log_set_level(LOG_LEVEL_DEBUG); + + struct pumpnet_prinet_proxy_options options; + + if (!pumpnet_prinet_proxy_options_init(argc, argv, &options)) { + return -1; + } + + util_log_set_file(options.log.file, false); + util_log_set_level(options.log.level); + + pumpnet_lib_prinet_init( + options.proxy.pumpnet.game, + options.proxy.pumpnet.server, + options.proxy.pumpnet.machine_id, + options.proxy.pumpnet.cert_dir_path, + options.proxy.pumpnet.verbose_log_output); + + log_info("Proxy, source (listen/sink) %s:%d -> pumpnet server %s (game: %d)", + options.proxy.source.addr, + options.proxy.source.port, + options.proxy.pumpnet.server, + options.proxy.pumpnet.game); + + sec_prinet_init( + (const uint8_t*) _binary_prime_public_key_start, + ((uintptr_t) &_binary_prime_public_key_end - + (uintptr_t) &_binary_prime_public_key_start), + (const uint8_t*) _binary_prime_private_key_start, + ((uintptr_t) &_binary_prime_private_key_end - + (uintptr_t) &_binary_prime_private_key_start)); + + _source_sock = util_sock_tcp_open_bind_listen2( + options.proxy.source.addr, + options.proxy.source.port, + NUM_CONNECTIONS); + + if (_source_sock == INVALID_SOCK_HANDLE) { + return -3; + } + + log_info("Running proxy loop"); + + signal(SIGINT, _sigint_handler); + + while (true) { + int source_con = INVALID_SOCK_HANDLE; + struct pumpnet_prinet_proxy_packet* source_req = NULL; + struct util_iobuf pumpnet_data_req; + struct util_iobuf pumpnet_data_resp; + struct pumpnet_prinet_proxy_packet* source_resp = NULL; + + source_con = util_sock_tcp_wait_and_accept_remote_connection(_source_sock); + + log_debug("Received connection: %X", source_con); + + if (source_con == INVALID_SOCK_HANDLE) { + log_error("Waiting and accepting source connection failed"); + goto cleanup_iteration; + } + + source_req = _recv_request_source(source_con); + + if (!source_req) { + log_error("Receiving request from source failed"); + goto cleanup_iteration; + } + + size_t source_data_len = pumpnet_prinet_proxy_packet_get_data_len(source_req); + util_iobuf_alloc(&pumpnet_data_req, source_data_len); + + if (!_transform_data_request(source_req, &pumpnet_data_req)) { + log_error("Transforming data request for destination failed"); + goto cleanup_iteration; + } + + util_iobuf_alloc(&pumpnet_data_resp, PUMPNET_MAX_RESP_SIZE); + + ssize_t pumpnet_recv_size = pumpnet_lib_prinet_msg( + pumpnet_data_req.bytes, + pumpnet_data_req.pos, + pumpnet_data_resp.bytes, + pumpnet_data_resp.nbytes); + + if (pumpnet_recv_size < 0) { + log_error("Request to pumpnet failed"); + goto cleanup_iteration; + } + + // Communicate actual data size + pumpnet_data_resp.pos = pumpnet_recv_size; + + source_resp = pumpnet_prinet_proxy_packet_alloc_response(pumpnet_data_resp.pos); + + if (!_transform_data_response(source_req, &pumpnet_data_resp, source_resp)) { + log_error("Transforming data response for source failed"); + goto cleanup_iteration; + } + + if (!_send_response_source(source_con, source_resp)) { + log_error("Sending response to source failed"); + goto cleanup_iteration; + } + + cleanup_iteration: + if (source_con != INVALID_SOCK_HANDLE) { + util_sock_tcp_close(source_con); + } + + if (source_req != NULL) { + util_xfree((void**) &source_req); + } + + if (pumpnet_data_req.bytes != NULL) { + util_iobuf_free(&pumpnet_data_req); + } + + if (pumpnet_data_resp.bytes != NULL) { + util_iobuf_free(&pumpnet_data_resp); + } + + if (source_resp != NULL) { + util_xfree((void**) &source_resp); + } + } +} \ No newline at end of file diff --git a/src/main/pumpnet/prinet-proxy/options.c b/src/main/pumpnet/prinet-proxy/options.c new file mode 100644 index 0000000..210089c --- /dev/null +++ b/src/main/pumpnet/prinet-proxy/options.c @@ -0,0 +1,122 @@ +#include "pumpnet/prinet-proxy/options.h" + +#include "util/options.h" + +#define PUMPNET_PRINET_PROXY_OPTIONS_STR_PROXY_SOURCE_ADDR "proxy.source.addr" +#define PUMPNET_PRINET_PROXY_OPTIONS_STR_PROXY_SOURCE_PORT "proxy.source.port" +#define PUMPNET_PRINET_PROXY_OPTIONS_STR_PROXY_PUMPNET_SERVER "proxy.pumpnet.server" +#define PUMPNET_PRINET_PROXY_OPTIONS_STR_PROXY_PUMPNET_GAME "proxy.pumpnet.game" +#define PUMPNET_PRINET_PROXY_OPTIONS_STR_PROXY_PUMPNET_MACHINE_ID "proxy.pumpnet.machine_id" +#define PUMPNET_PRINET_PROXY_OPTIONS_STR_PROXY_PUMPNET_VERBOSE_LOG_OUTPUT "proxy.pumpnet.verbose_log_output" +#define PUMPNET_PRINET_PROXY_OPTIONS_STR_PROXY_PUMPNET_CERT_DIR_PATH "proxy.pumpnet.cert_dir_path" +#define PUMPNET_PRINET_PROXY_OPTIONS_STR_PATCH_UTIL_LOG_FILE "util.log.file" +#define PUMPNET_PRINET_PROXY_OPTIONS_STR_PATCH_UTIL_LOG_LEVEL "util.log.level" + +const struct util_options_def _options_def[] = { + { + .name = PUMPNET_PRINET_PROXY_OPTIONS_STR_PROXY_SOURCE_ADDR, + .description = "IPV4 of the source address to listen to for incoming connections, e.g. 0.0.0.0", + .param = 's', + .type = UTIL_OPTIONS_TYPE_STR, + .default_value.str = "0.0.0.0", + }, + { + .name = PUMPNET_PRINET_PROXY_OPTIONS_STR_PROXY_SOURCE_PORT, + .description = "Port of the source to listen to for incoming connections, e.g. 1234", + .param = 'p', + .type = UTIL_OPTIONS_TYPE_INT, + // stock port used on prime + .default_value.i = 60000, + }, + { + .name = PUMPNET_PRINET_PROXY_OPTIONS_STR_PROXY_PUMPNET_SERVER, + .description = "Address (IPV4 or URL) and port of pumpnet server", + .param = 'n', + .type = UTIL_OPTIONS_TYPE_STR, + .default_value.str = "127.0.0.1:8080", + }, + { + .name = PUMPNET_PRINET_PROXY_OPTIONS_STR_PROXY_PUMPNET_MACHINE_ID, + .description = "Machine id for pumpnet", + .param = 'm', + .type = UTIL_OPTIONS_TYPE_STR, + .default_value.str = "0102030405060708", + }, + { + .name = PUMPNET_PRINET_PROXY_OPTIONS_STR_PROXY_PUMPNET_GAME, + .description = "Game ID required for server endpoint to talk to, e.g. prime (27), prime2 (28), xx(29)", + .param = 'g', + .type = UTIL_OPTIONS_TYPE_INT, + .default_value.i = ASSET_GAME_VERSION_PRIME, + }, + { + .name = PUMPNET_PRINET_PROXY_OPTIONS_STR_PROXY_PUMPNET_VERBOSE_LOG_OUTPUT, + .description = "Enable verbose log output for pumpnet related things, e.g. logging network traffic", + .param = 'v', + .type = UTIL_OPTIONS_TYPE_BOOL, + .default_value.b = false, + }, + { + .name = PUMPNET_PRINET_PROXY_OPTIONS_STR_PROXY_PUMPNET_CERT_DIR_PATH, + .description = "Path to a folder containing the client key, certificate and CA bundle to enable https communication", + .param = 'c', + .type = UTIL_OPTIONS_TYPE_STR, + .default_value.str = NULL, + }, + { + .name = PUMPNET_PRINET_PROXY_OPTIONS_STR_PATCH_UTIL_LOG_FILE, + .description = "Print the log output to the specified file", + .param = 'o', + .type = UTIL_OPTIONS_TYPE_STR, + .default_value.str = "pri-crypt-proxy.log", + }, + { + .name = PUMPNET_PRINET_PROXY_OPTIONS_STR_PATCH_UTIL_LOG_LEVEL, + .description = "Set the log level (0-4)", + .param = 'l', + .type = UTIL_OPTIONS_TYPE_INT, + .default_value.i = LOG_LEVEL_DEBUG, + }, +}; + +const struct util_options_defs _options_defs = { + .usage_header = + "Pumptools prinet-proxy for Pump It Up: Prime, Prime2 and XX, build " __DATE__ " " __TIME__ ", gitrev " STRINGIFY(GITREV) "\n" + "Usage: ./pumpnet-prinet-proxy --options pumpnet-prinet-proxy.conf", + .usage_param = 'h', + .defs = _options_def, + .ndefs = lengthof(_options_def) +}; + +bool pumpnet_prinet_proxy_options_init(int argc, char** argv, struct pumpnet_prinet_proxy_options* options) +{ + log_assert(argv); + log_assert(options); + + struct util_options_opts* options_opt; + + util_options_init(argc, argv); + options_opt = util_options_get(&_options_defs); + + if (!options_opt) { + return false; + } + + options->proxy.source.addr = util_options_get_str(options_opt, PUMPNET_PRINET_PROXY_OPTIONS_STR_PROXY_SOURCE_ADDR); + options->proxy.source.port = util_options_get_int(options_opt, PUMPNET_PRINET_PROXY_OPTIONS_STR_PROXY_SOURCE_PORT); + options->proxy.pumpnet.server = util_options_get_str(options_opt, PUMPNET_PRINET_PROXY_OPTIONS_STR_PROXY_PUMPNET_SERVER); + options->proxy.pumpnet.machine_id = strtoull( + util_options_get_str(options_opt, PUMPNET_PRINET_PROXY_OPTIONS_STR_PROXY_PUMPNET_MACHINE_ID), + NULL, + 16); + options->proxy.pumpnet.game = util_options_get_int(options_opt, PUMPNET_PRINET_PROXY_OPTIONS_STR_PROXY_PUMPNET_GAME); + options->proxy.pumpnet.verbose_log_output = + util_options_get_bool(options_opt, PUMPNET_PRINET_PROXY_OPTIONS_STR_PROXY_PUMPNET_VERBOSE_LOG_OUTPUT); + options->proxy.pumpnet.cert_dir_path = + util_options_get_str(options_opt, PUMPNET_PRINET_PROXY_OPTIONS_STR_PROXY_PUMPNET_CERT_DIR_PATH); + options->log.file = util_options_get_str(options_opt, PUMPNET_PRINET_PROXY_OPTIONS_STR_PATCH_UTIL_LOG_FILE); + options->log.level = + (enum util_log_level) util_options_get_int(options_opt, PUMPNET_PRINET_PROXY_OPTIONS_STR_PATCH_UTIL_LOG_LEVEL); + + return true; +} \ No newline at end of file diff --git a/src/main/pumpnet/prinet-proxy/options.h b/src/main/pumpnet/prinet-proxy/options.h new file mode 100644 index 0000000..df865d9 --- /dev/null +++ b/src/main/pumpnet/prinet-proxy/options.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include + +#include "asset/game-version.h" + +#include "util/log.h" + +struct pumpnet_prinet_proxy_options { + struct proxy { + struct source { + const char* addr; + uint16_t port; + } source; + + struct pumpnet { + const char* server; + enum asset_game_version game; + uint64_t machine_id; + bool verbose_log_output; + const char* cert_dir_path; + } pumpnet; + } proxy; + + struct log { + const char* file; + enum util_log_level level; + } log; +}; + +bool pumpnet_prinet_proxy_options_init(int argc, char** argv, struct pumpnet_prinet_proxy_options* options); diff --git a/src/main/pumpnet/prinet-proxy/packet.c b/src/main/pumpnet/prinet-proxy/packet.c new file mode 100644 index 0000000..200878b --- /dev/null +++ b/src/main/pumpnet/prinet-proxy/packet.c @@ -0,0 +1,43 @@ +#include + +#include "pumpnet/prinet-proxy/packet.h" + +#include "util/log.h" +#include "util/mem.h" + +static const size_t HEADER_SIZE = sizeof(uint32_t) + sizeof(uint8_t) * SEC_PRINET_NOUNCE_LEN; + +struct pumpnet_prinet_proxy_packet* pumpnet_prinet_proxy_packet_alloc(size_t data_len) +{ + struct pumpnet_prinet_proxy_packet* packet; + + packet = util_xmalloc(HEADER_SIZE + data_len); + + packet->length = HEADER_SIZE + data_len; + + return packet; +} + +struct pumpnet_prinet_proxy_packet* pumpnet_prinet_proxy_packet_alloc_response(size_t data_len) +{ + // + SEC_PRINET_MACBYTES_LEN expected on response only. + // Request does not have this included at the end of the game specific data + return pumpnet_prinet_proxy_packet_alloc(data_len + SEC_PRINET_MACBYTES_LEN); +} + +size_t pumpnet_prinet_proxy_packet_get_header_len() +{ + return HEADER_SIZE; +} + +size_t pumpnet_prinet_proxy_packet_get_data_len(const struct pumpnet_prinet_proxy_packet* packet) +{ + log_assert(packet); + + if (packet->length < HEADER_SIZE) { + log_error("Packet with size less than header size"); + return 0; + } + + return packet->length - HEADER_SIZE; +} \ No newline at end of file diff --git a/src/main/pumpnet/prinet-proxy/packet.h b/src/main/pumpnet/prinet-proxy/packet.h new file mode 100644 index 0000000..e73bb4f --- /dev/null +++ b/src/main/pumpnet/prinet-proxy/packet.h @@ -0,0 +1,20 @@ +#pragma once + +#include +#include + +#include "sec/prinet/prinet.h" + +struct pumpnet_prinet_proxy_packet { + uint32_t length; + uint8_t nounce[SEC_PRINET_NOUNCE_LEN]; + uint8_t data[]; +} __attribute__((__packed__)); + +struct pumpnet_prinet_proxy_packet* pumpnet_prinet_proxy_packet_alloc(size_t data_len); + +struct pumpnet_prinet_proxy_packet* pumpnet_prinet_proxy_packet_alloc_response(size_t data_len); + +size_t pumpnet_prinet_proxy_packet_get_header_len(); + +size_t pumpnet_prinet_proxy_packet_get_data_len(const struct pumpnet_prinet_proxy_packet* packet); \ No newline at end of file diff --git a/src/main/pumpnet/prinet-proxy/prime.private.key b/src/main/pumpnet/prinet-proxy/prime.private.key new file mode 100755 index 0000000..a624ea3 --- /dev/null +++ b/src/main/pumpnet/prinet-proxy/prime.private.key @@ -0,0 +1 @@ +“²ø à•I߯þ6Væ­œšS›öcÓ|ð¤æ)<¿údV \ No newline at end of file diff --git a/src/main/pumpnet/prinet-proxy/prime.public.key b/src/main/pumpnet/prinet-proxy/prime.public.key new file mode 100755 index 0000000..acb180d --- /dev/null +++ b/src/main/pumpnet/prinet-proxy/prime.public.key @@ -0,0 +1,2 @@ +°ë�GQ +. 2í/ÀùÔ눰Ê-½çÖNâЃL;¶û1U \ No newline at end of file