diff --git a/CMakeLists.txt b/CMakeLists.txt index 1a3cc1b..7c15824 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -35,6 +35,10 @@ option(UDPCAP_BUILD_SAMPLES "Build project samples" ON) +option(UDPCAP_BUILD_TESTS + "Build the udpcap GTests. Requires GTest::GTest to be available." + OFF) + option(UDPCAP_THIRDPARTY_ENABLED "Enable building against the builtin dependencies" ON) @@ -57,6 +61,12 @@ cmake_dependent_option(UDPCAP_THIRDPARTY_USE_BUILTIN_ASIO "UDPCAP_THIRDPARTY_ENABLED" OFF) +cmake_dependent_option(UDPCAP_THIRDPARTY_USE_BUILTIN_GTEST + "Fetch and build tests against a predefined version of GTest. If disabled, the targets have to be provided externally." + ON + "UDPCAP_THIRDPARTY_ENABLED AND UDPCAP_BUILD_TESTS" + OFF) + # Module path for finding udpcap list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/thirdparty/udpcap/modules) @@ -75,6 +85,12 @@ if (UDPCAP_THIRDPARTY_USE_BUILTIN_ASIO) include(thirdparty/asio/asio_make_available.cmake) endif() +#--- Fetch GTest ------------------------------- +if (UDPCAP_THIRDPARTY_USE_BUILTIN_GTEST) + include(thirdparty/GTest/GTest_make_available.cmake) +endif() + + #---------------------------------------------- # Set Debug postfix @@ -94,6 +110,11 @@ if (UDPCAP_BUILD_SAMPLES) add_subdirectory(samples/asio_sender_unicast) endif() +# Tests +if (UDPCAP_BUILD_TESTS) + enable_testing() + add_subdirectory(tests/udpcap_test) +endif() # Make this package available for packing with CPack include("${CMAKE_CURRENT_LIST_DIR}/cpack_config.cmake") diff --git a/README.md b/README.md index 4e95538..c7a8f54 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ Udpcap has a very simple API with strong similarities to other well-known socket int main() { - // Create a Udpcap socket and bind it to a port. For this exampel we want to + // Create a Udpcap socket and bind it to a port. For this example we want to // receive data from any local or remote source and therefore not bind to an // IP address. @@ -70,13 +70,23 @@ int main() for (;;) { + // Allocate a buffer for the received datagram. The size of the buffer + // should be large enough to hold the largest possible datagram. + std::vector datagram(65535); + + // Create an error code object to hold the error code if an error occurs. + Udpcap::Error error = Udpcap::Error::OK; + // Receive a datagram from the Socket. This is a blocking // operation. The operation will return once a datagram has been received, // the socket was closed by another thread or an error occured. - std::vector received_datagram = socket.receiveDatagram(); + size_t num_bytes = socket.receiveDatagram(datagram.data(), datagram.size(), error); + + // Resize the buffer to the actual size of the received datagram. + datagram.resize(num_bytes); - std::cout << "Received " << received_datagram.size() << " bytes: " - << std::string(received_datagram.data(), received_datagram.size()) + std::cout << "Received " << datagram.size() << " bytes: " + << std::string(datagram.data(), datagram.size()) << std::endl; } @@ -117,10 +127,12 @@ You can set the following CMake Options to control how Udpcap is supposed to bui **Option** | **Type** | **Default** | **Explanation** | |----------------------------------------------|----------|-------------|-----------------------------------------------------------------------------------------------------------------| | `UDPCAP_BUILD_SAMPLES` | `BOOL` | `ON` | Build the Udpcap (and asio) samples for sending and receiving dummy data | +| `UDPCAP_BUILD_TESTS` | `BOOL` | `OFF` | Build the udpcap GTests. Requires GTest::GTest to be available. | | `UDPCAP_THIRDPARTY_ENABLED` | `BOOL` | `ON` | Activate / Deactivate the usage of integrated dependencies. | | `UDPCAP_THIRDPARTY_USE_BUILTIN_NPCAP` | `BOOL` | `ON` | Fetch and build against an integrated Version of the npcap SDK.
Only available if `UDPCAP_THIRDPARTY_ENABLED=ON` | | `UDPCAP_THIRDPARTY_USE_BUILTIN_PCAPPLUSPLUS` | `BOOL` | `ON` | Fetch and build against an integrated Version of Pcap++.
_Only available if `UDPCAP_THIRDPARTY_ENABLED=ON`_ | | `UDPCAP_THIRDPARTY_USE_BUILTIN_ASIO` | `BOOL` | `ON` | Fetch and build against an integrated Version of asio.
Only available if `UDPCAP_THIRDPARTY_ENABLED=ON` | +| `UDPCAP_THIRDPARTY_USE_BUILTIN_GTEST` | `BOOL` | `ON` | Fetch and build tests against a predefined version of GTest. If disabled, the targets have to be provided externally.
Only available if `UDPCAP_THIRDPARTY_ENABLED=ON` and `UDPCAP_BUILD_TESTS=ON`| | `BUILD_SHARED_LIBS` | `BOOL` | | Not a udpcap option, but use this to control whether you want to have a static or shared library | # How to integrate Udpcap in your project diff --git a/samples/udpcap_receiver_multicast/src/main.cpp b/samples/udpcap_receiver_multicast/src/main.cpp index f4ff5b9..7346947 100644 --- a/samples/udpcap_receiver_multicast/src/main.cpp +++ b/samples/udpcap_receiver_multicast/src/main.cpp @@ -73,11 +73,11 @@ int main() return 1; } - // 5) Receive data from the socket + // 3) Receive data from the socket // - // There are 2 receiveDatagram() functions available. One of them returns - // the data as std::vector, the other expects a pointer to pre-allocated - // memory along with the maximum size. + // The receiveDatagram() function is used to receive data from the socket. + // It requires the application to allocate memory for the received data. + // If an error occurs, the error object is set accordingly. // // The socket.receiveDatagram() function is blocking. In this example we // can use the applications' main thread to wait for incoming data. @@ -91,17 +91,24 @@ int main() Udpcap::HostAddress sender_address; uint16_t sender_port(0); + // Allocate memory for the received datagram (with the maximum possible udp datagram size) + std::vector received_datagram(65536); + + // Initialize error object + Udpcap::Error error = Udpcap::Error::OK; + // Blocking receive a datagram - std::vector received_datagram = socket.receiveDatagram(&sender_address, &sender_port); + size_t received_bytes = socket.receiveDatagram(received_datagram.data(), received_datagram.size(), &sender_address, &sender_port, error); - if (sender_address.isValid()) + if (error) { - std::cout << "Received " << received_datagram.size() << " bytes from " << sender_address.toString() << ":" << sender_port << ": " << std::string(received_datagram.data(), received_datagram.size()) << std::endl; - } - else - { - std::cerr << "ERROR: Failed to receive data from Udpcap Socket" << std::endl; + std::cerr << "ERROR while receiving data:" << error.ToString() << std::endl; + return 1; } + + // Shrink the received_datagram to the actual size + received_datagram.resize(received_bytes); + std::cout << "Received " << received_datagram.size() << " bytes from " << sender_address.toString() << ":" << sender_port << ": " << std::string(received_datagram.data(), received_datagram.size()) << std::endl; } return 0; diff --git a/samples/udpcap_receiver_unicast/src/main.cpp b/samples/udpcap_receiver_unicast/src/main.cpp index 932eb41..c96bd8c 100644 --- a/samples/udpcap_receiver_unicast/src/main.cpp +++ b/samples/udpcap_receiver_unicast/src/main.cpp @@ -58,9 +58,9 @@ int main() // 3) Receive data from the socket // - // There are 2 receiveDatagram() functions available. One of them returns - // the data as std::vector, the other expects a pointer to pre-allocated - // memory along with the maximum size. + // The receiveDatagram() function is used to receive data from the socket. + // It requires the application to allocate memory for the received data. + // If an error occurs, the error object is set accordingly. // // The socket.receiveDatagram() function is blocking. In this example we // can use the applications' main thread to wait for incoming data. @@ -74,17 +74,24 @@ int main() Udpcap::HostAddress sender_address; uint16_t sender_port(0); + // Allocate memory for the received datagram (with the maximum possible udp datagram size) + std::vector received_datagram(65536); + + // Initialize error object + Udpcap::Error error = Udpcap::Error::OK; + // Blocking receive a datagram - std::vector received_datagram = socket.receiveDatagram(&sender_address, &sender_port); + size_t received_bytes = socket.receiveDatagram(received_datagram.data(), received_datagram.size(), &sender_address, &sender_port, error); - if (sender_address.isValid()) + if (error) { - std::cout << "Received " << received_datagram.size() << " bytes from " << sender_address.toString() << ":" << sender_port << ": " << std::string(received_datagram.data(), received_datagram.size()) << std::endl; - } - else - { - std::cerr << "ERROR: Failed to receive data from Udpcap Socket" << std::endl; + std::cerr << "ERROR while receiving data:" << error.ToString() << std::endl; + return 1; } + + // Shrink the received_datagram to the actual size + received_datagram.resize(received_bytes); + std::cout << "Received " << received_datagram.size() << " bytes from " << sender_address.toString() << ":" << sender_port << ": " << std::string(received_datagram.data(), received_datagram.size()) << std::endl; } return 0; diff --git a/tests/udpcap_test/CMakeLists.txt b/tests/udpcap_test/CMakeLists.txt new file mode 100644 index 0000000..e87e86d --- /dev/null +++ b/tests/udpcap_test/CMakeLists.txt @@ -0,0 +1,44 @@ +# =========================== LICENSE ================================= +# +# Copyright (C) 2016 - 2022 Continental Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# =========================== LICENSE ================================= + +cmake_minimum_required(VERSION 3.13) + +project(udpcap_test) + +set(CMAKE_FIND_PACKAGE_PREFER_CONFIG TRUE) + +find_package(udpcap REQUIRED) +find_package(GTest REQUIRED) +find_package(asio REQUIRED) + +set(sources + src/atomic_signalable.h + src/udpcap_test.cpp +) + +add_executable (${PROJECT_NAME} + ${sources} +) + +target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_14) + +target_link_libraries (${PROJECT_NAME} + udpcap::udpcap + GTest::gtest_main + $ +) diff --git a/tests/udpcap_test/src/atomic_signalable.h b/tests/udpcap_test/src/atomic_signalable.h new file mode 100644 index 0000000..237a59d --- /dev/null +++ b/tests/udpcap_test/src/atomic_signalable.h @@ -0,0 +1,205 @@ +/******************************************************************************** + * Copyright (c) 2024 Continental Corporation + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include +#include +#include +#include + +template +class atomic_signalable +{ +public: + atomic_signalable(T initial_value) : value(initial_value) {} + + atomic_signalable& operator=(const T new_value) + { + std::lock_guard lock(mutex); + value = new_value; + cv.notify_all(); + return *this; + } + + T operator++() + { + std::lock_guard lock(mutex); + T newValue = ++value; + cv.notify_all(); + return newValue; + } + + T operator++(T) + { + std::lock_guard lock(mutex); + T oldValue = value++; + cv.notify_all(); + return oldValue; + } + + T operator--() + { + std::lock_guard lock(mutex); + T newValue = --value; + cv.notify_all(); + return newValue; + } + + T operator--(T) + { + std::lock_guard lock(mutex); + T oldValue = value--; + cv.notify_all(); + return oldValue; + } + + T operator+=(const T& other) + { + std::lock_guard lock(mutex); + value += other; + cv.notify_all(); + return value; + } + + T operator-=(const T& other) + { + std::lock_guard lock(mutex); + value -= other; + cv.notify_all(); + return value; + } + + T operator*=(const T& other) + { + std::lock_guard lock(mutex); + value *= other; + cv.notify_all(); + return value; + } + + T operator/=(const T& other) + { + std::lock_guard lock(mutex); + value /= other; + cv.notify_all(); + return value; + } + + T operator%=(const T& other) + { + std::lock_guard lock(mutex); + value %= other; + cv.notify_all(); + return value; + } + + template + bool wait_for(Predicate predicate, std::chrono::milliseconds timeout) + { + std::unique_lock lock(mutex); + return cv.wait_for(lock, timeout, [&]() { return predicate(value); }); + } + + T get() const + { + std::lock_guard lock(mutex); + return value; + } + + bool operator==(T other) const + { + std::lock_guard lock(mutex); + return value == other; + } + + bool operator==(const atomic_signalable& other) const + { + std::lock_guard lock_this(mutex); + std::lock_guard lock_other(other.mutex); + return value == other.value; + } + + bool operator!=(T other) const + { + std::lock_guard lock(mutex); + return value != other; + } + + bool operator<(T other) const + { + std::lock_guard lock(mutex); + return value < other; + } + + bool operator<=(T other) const + { + std::lock_guard lock(mutex); + return value <= other; + } + + bool operator>(T other) const + { + std::lock_guard lock(mutex); + return value > other; + } + + bool operator>=(T other) const + { + std::lock_guard lock(mutex); + return value >= other; + } + +private: + T value; + std::condition_variable cv; + mutable std::mutex mutex; +}; + + +template +bool operator==(const T& other, const atomic_signalable& atomic) +{ + return atomic == other; +} + +template +bool operator!=(const T& other, const atomic_signalable& atomic) +{ + return atomic != other; +} + +template +bool operator<(const T& other, const atomic_signalable& atomic) +{ + return atomic > other; +} + +template +bool operator<=(const T& other, const atomic_signalable& atomic) +{ + return atomic >= other; +} + +template +bool operator>(const T& other, const atomic_signalable& atomic) +{ + return atomic < other; +} + +template +bool operator>=(const T& other, const atomic_signalable& atomic) +{ + return atomic <= other; +} diff --git a/tests/udpcap_test/src/udpcap_test.cpp b/tests/udpcap_test/src/udpcap_test.cpp new file mode 100644 index 0000000..6b1358c --- /dev/null +++ b/tests/udpcap_test/src/udpcap_test.cpp @@ -0,0 +1,376 @@ +/* =========================== LICENSE ================================= + * + * Copyright (C) 2016 - 2022 Continental Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * =========================== LICENSE ================================= + */ + +#include + +#include +#include + +#include + +#include "atomic_signalable.h" + +TEST(udpcap, RAII) +{ + // Create a udpcap socket + Udpcap::UdpcapSocket udpcap_socket; + ASSERT_TRUE(udpcap_socket.isValid()); + + // Delete the socket +} + +TEST(udpcap, RAIIWithClose) +{ + // Create a udpcap socket + Udpcap::UdpcapSocket udpcap_socket; + ASSERT_TRUE(udpcap_socket.isValid()); + + // bind the socket + bool success = udpcap_socket.bind(Udpcap::HostAddress::Any(), 14000); + ASSERT_TRUE(success); + + // Close the socket + udpcap_socket.close(); +} + +TEST(udpcap, RAIIWithSomebodyWaiting) +{ + // Create a udpcap socket + Udpcap::UdpcapSocket udpcap_socket; + ASSERT_TRUE(udpcap_socket.isValid()); + + // bind the socket + bool success = udpcap_socket.bind(Udpcap::HostAddress::Any(), 14000); + ASSERT_TRUE(success); + + // Blocking receive a datagram + std::thread receive_thread([&udpcap_socket]() + { + // Create buffer with max udp datagram size + std::vector received_datagram; + received_datagram.resize(65536); + + Udpcap::Error error = Udpcap::Error::ErrorCode::GENERIC_ERROR; + + // blocking receive + size_t received_bytes = udpcap_socket.receiveDatagram(received_datagram.data(), received_datagram.size(), 0, error); + + // Check that we didn't receive any bytes + ASSERT_EQ(received_bytes, 0); + + // TODO: check actual error, which should indicate that the socket is closed + ASSERT_TRUE(bool(error)); + + }); + + // Close the socket + udpcap_socket.close(); + + // Join the thread + receive_thread.join(); + + // Delete the socket +} + +TEST(udpcap, SimpleReceive) +{ + atomic_signalable received_messages(0); + + // Create a udpcap socket + Udpcap::UdpcapSocket udpcap_socket; + ASSERT_TRUE(udpcap_socket.isValid()); + + { + bool success = udpcap_socket.bind(Udpcap::HostAddress::Any(), 14000); + ASSERT_TRUE(success); + } + + // Blocking receive a datagram + std::thread receive_thread([&udpcap_socket, &received_messages]() + { + // Initialize variables for the sender's address and port + Udpcap::HostAddress sender_address; + uint16_t sender_port(0); + Udpcap::Error error = Udpcap::Error::ErrorCode::GENERIC_ERROR; + + // Allocate buffer with max udp datagram size + std::vector received_datagram; + received_datagram.resize(65536); + + // blocking receive + size_t received_bytes = udpcap_socket.receiveDatagram(received_datagram.data(), received_datagram.size(), &sender_address, &sender_port, error); + received_datagram.resize(received_bytes); + + // No error must have occurred + ASSERT_FALSE(bool(error)); + + // Check if the received datagram is valid and contains "Hello World" + ASSERT_FALSE(received_datagram.empty()); + ASSERT_EQ(std::string(received_datagram.data(), received_datagram.size()), "Hello World"); + + received_messages++; + }); + + // Create an asio UDP sender socket + asio::io_service io_service; + + const asio::ip::udp::endpoint endpoint(asio::ip::make_address("127.0.0.1"), 14000); + asio::ip::udp::socket asio_socket(io_service, endpoint.protocol()); + + std::string buffer_string = "Hello World"; + asio_socket.send_to(asio::buffer(buffer_string), endpoint); + + // Wait max 100ms for the receive thread to finish + received_messages.wait_for([](int value) { return value >= 1; }, std::chrono::milliseconds(100)); + + // Check if the received message counter is 1 + ASSERT_EQ(received_messages.get(), 1); + + asio_socket.close(); + udpcap_socket.close(); + + receive_thread.join(); +} + +TEST(udpcap, MultipleSmallPackages) +{ + constexpr int num_packages_to_send = 10; + constexpr std::chrono::milliseconds send_delay(1); + + atomic_signalable received_messages(0); + + // Create a udpcap socket + Udpcap::UdpcapSocket udpcap_socket; + ASSERT_TRUE(udpcap_socket.isValid()); + + // Bind the udpcap socket to all interfaces + { + bool success = udpcap_socket.bind(Udpcap::HostAddress::Any(), 14000); + ASSERT_TRUE(success); + } + + // Receive datagrams in a separate thread + std::thread receive_thread([&udpcap_socket, &received_messages, num_packages_to_send]() + { + while (true) + { + Udpcap::Error error = Udpcap::Error::ErrorCode::GENERIC_ERROR; + + // Allocate buffer with max udp datagram size + std::vector received_datagram; + received_datagram.resize(65536); + + // blocking receive + size_t received_bytes = udpcap_socket.receiveDatagram(received_datagram.data(), received_datagram.size(), error); + + if (error) + { + // TODO: Check that actual error reason + + // Indicates that somebody closed the socket + ASSERT_EQ(received_messages.get(), num_packages_to_send); + break; + } + + received_datagram.resize(received_bytes); + + // Check if the received datagram is valid and contains "Hello World" + ASSERT_FALSE(received_datagram.empty()); + ASSERT_EQ(std::string(received_datagram.data(), received_datagram.size()), "Hello World"); + + received_messages++; + } + }); + + // Create an asio UDP sender socket + asio::io_service io_service; + + const asio::ip::udp::endpoint endpoint(asio::ip::make_address("127.0.0.1"), 14000); + asio::ip::udp::socket asio_socket(io_service, endpoint.protocol()); + + std::string buffer_string = "Hello World"; + for (int i = 0; i < num_packages_to_send; i++) + { + asio_socket.send_to(asio::buffer(buffer_string), endpoint); + std::this_thread::sleep_for(send_delay); + } + + // Wait max 100ms for the receive thread to finish + received_messages.wait_for([num_packages_to_send](int value) { return value >= num_packages_to_send; }, std::chrono::milliseconds(100)); + + // Check if the received message counter is 1 + ASSERT_EQ(received_messages.get(), num_packages_to_send); + + asio_socket.close(); + udpcap_socket.close(); + + receive_thread.join(); +} + +TEST(udpcap, SimpleReceiveWithBuffer) +{ + atomic_signalable received_messages(0); + + // Create a udpcap socket + Udpcap::UdpcapSocket udpcap_socket; + ASSERT_TRUE(udpcap_socket.isValid()); + + { + bool success = udpcap_socket.bind(Udpcap::HostAddress::LocalHost(), 14000); + ASSERT_TRUE(success); + } + + // Create an asio UDP sender socket + asio::io_service io_service; + + const asio::ip::udp::endpoint endpoint(asio::ip::make_address("127.0.0.1"), 14000); + asio::ip::udp::socket asio_socket(io_service, endpoint.protocol()); + + // Send "Hello World" without currently polling the socket + std::string buffer_string = "Hello World"; + asio_socket.send_to(asio::buffer(buffer_string), endpoint); + + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + + // Receive the datagram + std::thread receive_thread([&udpcap_socket, &received_messages]() + { + Udpcap::Error error = Udpcap::Error::ErrorCode::GENERIC_ERROR; + + // Create buffer with max udp datagram size + std::vector received_datagram; + received_datagram.resize(65536); + + received_datagram.resize(udpcap_socket.receiveDatagram(received_datagram.data(), received_datagram.size(), error)); + + // No error must have occurred + ASSERT_FALSE(bool(error)); + + // Check if the received datagram is valid and contains "Hello World" + ASSERT_FALSE(received_datagram.empty()); + ASSERT_EQ(std::string(received_datagram.data(), received_datagram.size()), "Hello World"); + + received_messages++; + }); + + + // Wait max 100ms for the receive thread to finish + received_messages.wait_for([](int value) { return value >= 1; }, std::chrono::milliseconds(100)); + + // Check if the received message counter is 1 + ASSERT_EQ(received_messages.get(), 1); + + asio_socket.close(); + udpcap_socket.close(); + + receive_thread.join(); +} + +TEST(udpcap, DelayedPackageReceiveMultiplePackages) +{ + constexpr int num_packages_to_send = 100; // TODO: increase + constexpr int size_per_package = 1024; + constexpr std::chrono::milliseconds receive_delay(10); + + atomic_signalable received_messages(0); + + // Create a 1400 byte buffer for sending + std::vector buffer(size_per_package, 'a'); + + // Create a udpcap socket + Udpcap::UdpcapSocket udpcap_socket; + ASSERT_TRUE(udpcap_socket.isValid()); + + // Bind the udpcap socket to all interfaces + { + bool success = udpcap_socket.bind(Udpcap::HostAddress::Any(), 14000); + ASSERT_TRUE(success); + } + + // Receive datagrams in a separate thread + std::thread receive_thread([&udpcap_socket, &received_messages, num_packages_to_send, size_per_package, receive_delay]() + { + while (true) + { + // Initialize variables for the sender's address and port + Udpcap::HostAddress sender_address; + uint16_t sender_port(0); + + Udpcap::Error error = Udpcap::Error::ErrorCode::GENERIC_ERROR; + + std::vector received_datagram; + received_datagram.resize(65536); + + size_t bytes_received = udpcap_socket.receiveDatagram(received_datagram.data(), received_datagram.size(), 0, &sender_address, &sender_port, error); + received_datagram.resize(bytes_received); + + if (error) + { + // Indicates that somebody closed the socket + ASSERT_EQ(received_messages.get(), num_packages_to_send); + break; + } + + // Check if the received datagram is valid and contains "Hello World" + ASSERT_EQ(received_datagram.size(), size_per_package); + received_messages++; + + // Wait a bit, so we force the udpcap socket to buffer the datagrams + std::this_thread::sleep_for(receive_delay); + } + }); + + // Create an asio UDP sender socket + asio::io_service io_service; + + const asio::ip::udp::endpoint endpoint(asio::ip::make_address("127.0.0.1"), 14000); + asio::ip::udp::socket asio_socket(io_service, endpoint.protocol()); + + // Capture the start time + auto start_time = std::chrono::steady_clock::now(); + + // Send the buffers + for (int i = 0; i < num_packages_to_send; i++) + { + asio_socket.send_to(asio::buffer(buffer), endpoint); + } + + // Wait some time for the receive thread to finish + received_messages.wait_for([num_packages_to_send](int value) { return value >= num_packages_to_send; }, receive_delay * num_packages_to_send + std::chrono::milliseconds(1000)); + + // Check if the received message counter is equal to the sent messages + ASSERT_EQ(received_messages.get(), num_packages_to_send); + + // Capture the end time + auto end_time = std::chrono::steady_clock::now(); + + // TODO: check the entire delay + + asio_socket.close(); + udpcap_socket.close(); + + receive_thread.join(); +} + + +// TODO: Write a test that tests the Source Address and Source Port + +// TODO: Write a test that tests the timeout + +// TODO: test isclosed function \ No newline at end of file diff --git a/thirdparty/GTest/GTest_make_available.cmake b/thirdparty/GTest/GTest_make_available.cmake new file mode 100644 index 0000000..feb44a8 --- /dev/null +++ b/thirdparty/GTest/GTest_make_available.cmake @@ -0,0 +1,33 @@ +include(FetchContent) +FetchContent_Declare(GTest + GIT_REPOSITORY https://github.com/google/googletest.git + GIT_TAG origin/v1.14.x # This is not a Tag, but the release branch + CONFIGURE_COMMAND "" + BUILD_COMMAND "" + ) +FetchContent_GetProperties(GTest) +if(NOT gtest_POPULATED) + message(STATUS "Fetching GTest...") + FetchContent_Populate(GTest) +endif() +set(GTest_ROOT_DIR "${gtest_SOURCE_DIR}") + +# Googletest automatically forces MT instead of MD if we do not set this option. +if(MSVC) + set(gtest_force_shared_crt ON CACHE BOOL "My option" FORCE) + set(BUILD_GMOCK OFF CACHE BOOL "My option" FORCE) + set(INSTALL_GTEST OFF CACHE BOOL "My option" FORCE) +endif() + +add_subdirectory("${GTest_ROOT_DIR}" EXCLUDE_FROM_ALL) + +if(NOT TARGET GTest::gtest) + add_library(GTest::gtest ALIAS gtest) +endif() + +if(NOT TARGET GTest::gtest_main) + add_library(GTest::gtest_main ALIAS gtest_main) +endif() + +# Prepend googletest-module/FindGTest.cmake to Module Path +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/Modules/") \ No newline at end of file diff --git a/thirdparty/GTest/Modules/FindGTest.cmake b/thirdparty/GTest/Modules/FindGTest.cmake new file mode 100644 index 0000000..f62c081 --- /dev/null +++ b/thirdparty/GTest/Modules/FindGTest.cmake @@ -0,0 +1 @@ +set(GTest_FOUND TRUE CACHE BOOL "Found Google Test" FORCE) \ No newline at end of file diff --git a/udpcap/CMakeLists.txt b/udpcap/CMakeLists.txt index 0f03783..2da842e 100644 --- a/udpcap/CMakeLists.txt +++ b/udpcap/CMakeLists.txt @@ -36,6 +36,7 @@ include(GenerateExportHeader) # Public API include directory set (includes + include/udpcap/error.h include/udpcap/host_address.h include/udpcap/npcap_helpers.h include/udpcap/udpcap_socket.h diff --git a/udpcap/include/udpcap/error.h b/udpcap/include/udpcap/error.h new file mode 100644 index 0000000..517c53a --- /dev/null +++ b/udpcap/include/udpcap/error.h @@ -0,0 +1,116 @@ +/******************************************************************************** + * Copyright (c) 2024 Continental Corporation + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#pragma once + +#include + +namespace Udpcap +{ + class Error + { + ////////////////////////////////////////// + // Data model + ////////////////////////////////////////// + public: + enum ErrorCode + { + // Generic + OK, + GENERIC_ERROR, + + // NPCAP errors + NPCAP_NOT_INITIALIZED, + + // Socket errors + NOT_BOUND, + TIMEOUT, + SOCKET_CLOSED, + }; + + ////////////////////////////////////////// + // Constructor & Destructor + ////////////////////////////////////////// + public: + Error(ErrorCode error_code, const std::string& message) : error_code_(error_code), message_(message) {} + Error(ErrorCode error_code) : error_code_(error_code) {} + + // Copy constructor & assignment operator + Error(const Error& other) = default; + Error& operator=(const Error& other) = default; + + // Move constructor & assignment operator + Error(Error&& other) = default; + Error& operator=(Error&& other) = default; + + ~Error() = default; + + ////////////////////////////////////////// + // Public API + ////////////////////////////////////////// + public: + inline std::string GetDescription() const + { + switch (error_code_) + { + // Generic + case OK: return "OK"; break; + case GENERIC_ERROR: return "Error"; break; + + case NPCAP_NOT_INITIALIZED: return "Npcap not initialized"; break; + + case NOT_BOUND: return "Socket not bound"; break; + case TIMEOUT: return "Timeout"; break; + case SOCKET_CLOSED: return "Socket closed"; break; + + default: return "Unknown error"; + } + } + + inline std::string ToString() const + { + return (message_.empty() ? GetDescription() : GetDescription() + " (" + message_ + ")"); + } + + const inline std::string& GetMessage() const + { + return message_; + } + + ////////////////////////////////////////// + // Operators + ////////////////////////////////////////// + inline operator bool() const { return error_code_ != ErrorCode::OK; } + inline bool operator== (const Error& other) const { return error_code_ == other.error_code_; } + inline bool operator== (const ErrorCode other) const { return error_code_ == other; } + inline bool operator!= (const Error& other) const { return error_code_ != other.error_code_; } + inline bool operator!= (const ErrorCode other) const { return error_code_ != other; } + + inline Error& operator=(ErrorCode error_code) + { + error_code_ = error_code; + return *this; + } + + ////////////////////////////////////////// + // Member Variables + ////////////////////////////////////////// + private: + ErrorCode error_code_; + std::string message_; + }; + +} // namespace Udpcap diff --git a/udpcap/include/udpcap/udpcap_socket.h b/udpcap/include/udpcap/udpcap_socket.h index 35eae2d..e535766 100644 --- a/udpcap/include/udpcap/udpcap_socket.h +++ b/udpcap/include/udpcap/udpcap_socket.h @@ -21,6 +21,7 @@ #include #include +#include #include #include @@ -133,29 +134,6 @@ namespace Udpcap */ UDPCAP_EXPORT bool hasPendingDatagrams() const; - /** - * @brief Blocks until A packet arives and returns it as char-vector - */ - UDPCAP_EXPORT std::vector receiveDatagram(HostAddress* source_address = nullptr, uint16_t* source_port = nullptr); - - /** - * @brief Blocks for the given time until a packet arives and returns it as char-vector - * - * If the socket is not bound, this method will return immediatelly. - * If a source_adress or source_port is provided, these will be filled with - * the according information from the packet. If the given time elapses - * before a datagram was available, an empty vector is returned. - * - * @param timeout_ms [in]: Maximum time to wait for a datagram in ms - * @param source_address [out]: the sender address of the datagram - * @param source_port [out]: the sender port of the datagram - * - * @return The datagram binary data - */ - UDPCAP_EXPORT std::vector receiveDatagram(unsigned long timeout_ms, HostAddress* source_address = nullptr, uint16_t* source_port = nullptr); - - UDPCAP_EXPORT size_t receiveDatagram(char* data, size_t max_len, HostAddress* source_address = nullptr, uint16_t* source_port = nullptr); - /** * @brief Blocks for the given time until a packet arives and copies it to the given memory * @@ -163,16 +141,42 @@ namespace Udpcap * If a source_adress or source_port is provided, these will be filled with * the according information from the packet. If the given time elapses * before a datagram was available, no data is copied and 0 is returned. + * + * TODO: Document which error occurs in which case * * @param data [out]: The destination memory * @param max_len [in]: The maximum bytes available at the destination * @param timeout_ms [in]: Maximum time to wait for a datagram in ms * @param source_address [out]: the sender address of the datagram * @param source_port [out]: the sender port of the datagram + * @param error [out]: The error that occured * * @return The number of bytes copied to the data pointer */ - UDPCAP_EXPORT size_t receiveDatagram(char* data, size_t max_len, unsigned long timeout_ms, HostAddress* source_address = nullptr, uint16_t* source_port = nullptr); + UDPCAP_EXPORT size_t receiveDatagram(char* data + , size_t max_len + , unsigned long timeout_ms + , HostAddress* source_address + , uint16_t* source_port + , Udpcap::Error& error); + + // TODO: Copy documentation here + UDPCAP_EXPORT size_t receiveDatagram(char* data + , size_t max_len + , unsigned long timeout_ms + , Udpcap::Error& error); + + // TODO: Copy documentation here + UDPCAP_EXPORT size_t receiveDatagram(char* data + , size_t max_len + , Udpcap::Error& error); + + // TODO: Copy documentation here + UDPCAP_EXPORT size_t receiveDatagram(char* data + , size_t max_len + , HostAddress* source_address + , uint16_t* source_port + , Udpcap::Error& error); /** * @brief Joins the given multicast group @@ -222,6 +226,13 @@ namespace Udpcap */ UDPCAP_EXPORT void close(); + /** + * @brief Returns whether the socket is closed + * + * @return true, if the socket is closed + */ + UDPCAP_EXPORT bool isClosed() const; + private: /** This is where the actual implementation lies. But the implementation has * to include many nasty header files (e.g. Windows.h), which is why we only diff --git a/udpcap/src/udpcap_socket.cpp b/udpcap/src/udpcap_socket.cpp index 066c5a6..4b17893 100644 --- a/udpcap/src/udpcap_socket.cpp +++ b/udpcap/src/udpcap_socket.cpp @@ -45,10 +45,10 @@ namespace Udpcap bool UdpcapSocket::hasPendingDatagrams () const { return udpcap_socket_private_->hasPendingDatagrams(); } - std::vector UdpcapSocket::receiveDatagram (HostAddress* source_address, uint16_t* source_port) { return udpcap_socket_private_->receiveDatagram(source_address, source_port); } - std::vector UdpcapSocket::receiveDatagram (unsigned long timeout_ms, HostAddress* source_address, uint16_t* source_port) { return udpcap_socket_private_->receiveDatagram(timeout_ms, source_address, source_port); } - size_t UdpcapSocket::receiveDatagram (char* data, size_t max_len, HostAddress* source_address, uint16_t* source_port) { return udpcap_socket_private_->receiveDatagram(data, max_len, source_address, source_port); } - size_t UdpcapSocket::receiveDatagram (char* data, size_t max_len, unsigned long timeout_ms, HostAddress* source_address, uint16_t* source_port) { return udpcap_socket_private_->receiveDatagram(data, max_len, timeout_ms, source_address, source_port); } + size_t UdpcapSocket::receiveDatagram(char* data, size_t max_len, unsigned long timeout_ms, HostAddress* source_address, uint16_t* source_port, Udpcap::Error& error) { return udpcap_socket_private_->receiveDatagram(data, max_len, timeout_ms, source_address, source_port, error); } + size_t UdpcapSocket::receiveDatagram(char* data, size_t max_len, unsigned long timeout_ms, Udpcap::Error& error) { return udpcap_socket_private_->receiveDatagram(data, max_len, timeout_ms, nullptr, nullptr, error); } + size_t UdpcapSocket::receiveDatagram(char* data, size_t max_len, Udpcap::Error& error) { return udpcap_socket_private_->receiveDatagram(data, max_len, 0, nullptr, nullptr, error); } + size_t UdpcapSocket::receiveDatagram(char* data, size_t max_len, HostAddress* source_address, uint16_t* source_port, Udpcap::Error& error) { return udpcap_socket_private_->receiveDatagram(data, max_len, 0, source_address, source_port, error); } bool UdpcapSocket::joinMulticastGroup (const HostAddress& group_address) { return udpcap_socket_private_->joinMulticastGroup(group_address); } bool UdpcapSocket::leaveMulticastGroup (const HostAddress& group_address) { return udpcap_socket_private_->leaveMulticastGroup(group_address); } @@ -57,5 +57,6 @@ namespace Udpcap bool UdpcapSocket::isMulticastLoopbackEnabled () const { return udpcap_socket_private_->isMulticastLoopbackEnabled(); } void UdpcapSocket::close () { udpcap_socket_private_->close(); } + bool UdpcapSocket::isClosed () const { return udpcap_socket_private_->isClosed(); } } \ No newline at end of file diff --git a/udpcap/src/udpcap_socket_private.cpp b/udpcap/src/udpcap_socket_private.cpp index 18ce58f..345a31d 100644 --- a/udpcap/src/udpcap_socket_private.cpp +++ b/udpcap/src/udpcap_socket_private.cpp @@ -30,6 +30,8 @@ #include #include #include +#include +#include #include @@ -45,6 +47,7 @@ namespace Udpcap , bound_port_ (0) , multicast_loopback_enabled_(true) , receive_buffer_size_ (-1) + , pcap_devices_closed_ (false) { } @@ -86,12 +89,14 @@ namespace Udpcap // Valid address => Try to bind to address! + std::unique_lock pcap_devices_lists_lock(pcap_devices_lists_mutex_); + if (local_address.isLoopback()) { // Bind to localhost (We cannot find it by IP 127.0.0.1, as that IP is technically not even assignable to the loopback adapter). LOG_DEBUG(std::string("Opening Loopback device ") + GetLoopbackDeviceName()); - if (!openPcapDevice(GetLoopbackDeviceName())) + if (!openPcapDevice_nolock(GetLoopbackDeviceName())) { LOG_DEBUG(std::string("Bind error: Unable to bind to ") + GetLoopbackDeviceName()); close(); @@ -114,7 +119,7 @@ namespace Udpcap { LOG_DEBUG(std::string("Opening ") + dev.first + " (" + dev.second + ")"); - if (!openPcapDevice(dev.first)) + if (!openPcapDevice_nolock(dev.first)) { LOG_DEBUG(std::string("Bind error: Unable to bind to ") + dev.first); } @@ -134,7 +139,7 @@ namespace Udpcap LOG_DEBUG(std::string("Opening ") + dev.first + " (" + dev.second + ")"); - if (!openPcapDevice(dev.first)) + if (!openPcapDevice_nolock(dev.first)) { LOG_DEBUG(std::string("Bind error: Unable to bind to ") + dev.first); close(); @@ -144,7 +149,7 @@ namespace Udpcap // Also open loopback adapter. We always have to expect the local machine sending data to its own IP address. LOG_DEBUG(std::string("Opening Loopback device ") + GetLoopbackDeviceName()); - if (!openPcapDevice(GetLoopbackDeviceName())) + if (!openPcapDevice_nolock(GetLoopbackDeviceName())) { LOG_DEBUG(std::string("Bind error: Unable to open ") + GetLoopbackDeviceName()); close(); @@ -152,9 +157,10 @@ namespace Udpcap } } - bound_address_ = local_address; - bound_port_ = local_port; - bound_state_ = true; + bound_address_ = local_address; + bound_port_ = local_port; + bound_state_ = true; + pcap_devices_closed_ = false; for (auto& pcap_dev : pcap_devices_) { @@ -223,6 +229,19 @@ namespace Udpcap return false; } + // Lock the lists of open pcap devices in read-mode. We may use the handles, but not modify the lists themselfes. + const std::shared_lock pcap_devices_lists_lock(pcap_devices_lists_mutex_); + + { + const std::lock_guard pcap_callback_lock(pcap_devices_callback_mutex_); + if (pcap_devices_closed_) + { + // No open devices => fail! + LOG_DEBUG("Has Pending Datagrams error: Socket has been closed."); + return false; + } + } + if (pcap_win32_handles_.empty()) { // No open devices => fail! @@ -245,12 +264,12 @@ namespace Udpcap } - std::vector UdpcapSocketPrivate::receiveDatagram(HostAddress* source_address, uint16_t* source_port) + std::vector UdpcapSocketPrivate::receiveDatagram_OLD(HostAddress* source_address, uint16_t* source_port) { - return receiveDatagram(INFINITE, source_address, source_port); + return receiveDatagram_OLD(INFINITE, source_address, source_port); } - std::vector UdpcapSocketPrivate::receiveDatagram(unsigned long timeout_ms, HostAddress* source_address, uint16_t* source_port) + std::vector UdpcapSocketPrivate::receiveDatagram_OLD(unsigned long timeout_ms, HostAddress* source_address, uint16_t* source_port) { if (!is_valid_) { @@ -266,6 +285,9 @@ namespace Udpcap return{}; } + // Lock the lists of open pcap devices in read-mode. We may use the handles, but not modify the lists themselfes. + const std::shared_lock pcap_devices_lists_lock(pcap_devices_lists_mutex_); + if (pcap_win32_handles_.empty()) { // No open devices => fail! @@ -302,16 +324,29 @@ namespace Udpcap } } + std::cerr << "WaitForMultipleObjects START...\n"; const DWORD wait_result = WaitForMultipleObjects(num_handles, pcap_win32_handles_.data(), static_cast(false), remaining_time_to_wait_ms); + std::cerr << "WaitForMultipleObjects END...\n"; if ((wait_result >= WAIT_OBJECT_0) && wait_result <= (WAIT_OBJECT_0 + num_handles - 1)) { const int dev_index = (wait_result - WAIT_OBJECT_0); - callback_args.link_type_ = static_cast(pcap_datalink(pcap_devices_[dev_index].pcap_handle_)); - callback_args.ip_reassembly_ = ip_reassembly_[dev_index].get(); + { + // Lock the callback lock. While the callback is running, we cannot close the pcap handle, as that may invalidate the data pointer. + const std::lock_guard pcap_devices_callback_lock(pcap_devices_callback_mutex_); - pcap_dispatch(pcap_devices_[dev_index].pcap_handle_, 1, UdpcapSocketPrivate::PacketHandlerVector, reinterpret_cast(&callback_args)); + if (pcap_devices_closed_) + { + // TODO: Return an error + return {}; + } + + callback_args.link_type_ = static_cast(pcap_datalink(pcap_devices_[dev_index].pcap_handle_)); + callback_args.ip_reassembly_ = pcap_devices_ip_reassembly_[dev_index].get(); + + pcap_dispatch(pcap_devices_[dev_index].pcap_handle_, 100, UdpcapSocketPrivate::PacketHandlerVector, reinterpret_cast(&callback_args)); + } if (callback_args.success_) { @@ -329,19 +364,21 @@ namespace Udpcap } else if (wait_result == WAIT_FAILED) { - LOG_DEBUG("Receive error: WAIT_FAILED: " + std::to_string(GetLastError())); + LOG_DEBUG("Receive error: WAIT_FAILED: " + std::system_category().message(GetLastError())); + // TODO: Check if I can always just return here. This definitively happens when I close the socket, so I MUST return in certain cases. But I don't know if there may be cases when this happens without closing the socket. + return {}; } } while (wait_forever || (std::chrono::steady_clock::now() < wait_until)); return{}; } - size_t UdpcapSocketPrivate::receiveDatagram(char* data, size_t max_len, HostAddress* source_address, uint16_t* source_port) + size_t UdpcapSocketPrivate::receiveDatagram_OLD(char* data, size_t max_len, HostAddress* source_address, uint16_t* source_port) { - return receiveDatagram(data, max_len, INFINITE, source_address, source_port); + return receiveDatagram_OLD(data, max_len, INFINITE, source_address, source_port); } - size_t UdpcapSocketPrivate::receiveDatagram(char* data, size_t max_len, unsigned long timeout_ms, HostAddress* source_address, uint16_t* source_port) + size_t UdpcapSocketPrivate::receiveDatagram_OLD(char* data, size_t max_len, unsigned long timeout_ms, HostAddress* source_address, uint16_t* source_port) { if (!is_valid_) { @@ -357,6 +394,9 @@ namespace Udpcap return{}; } + // Lock the lists of open pcap devices in read-mode. We may use the handles, but not modify the lists themselfes. + const std::shared_lock pcap_devices_list_lock(pcap_devices_lists_mutex_); + if (pcap_win32_handles_.empty()) { // No open devices => fail! @@ -398,10 +438,21 @@ namespace Udpcap { const int dev_index = (wait_result - WAIT_OBJECT_0); - callback_args.link_type_ = static_cast(pcap_datalink(pcap_devices_[dev_index].pcap_handle_)); - callback_args.ip_reassembly_ = ip_reassembly_[dev_index].get(); + { + // Lock the callback lock. While the callback is running, we cannot close the pcap handle, as that may invalidate the data pointer. + const std::lock_guard pcap_devices_callback__lock(pcap_devices_callback_mutex_); + + if (pcap_devices_closed_) + { + // TODO: Return an error + return {}; + } - pcap_dispatch(pcap_devices_[dev_index].pcap_handle_, 1, UdpcapSocketPrivate::PacketHandlerRawPtr, reinterpret_cast(&callback_args)); + callback_args.link_type_ = static_cast(pcap_datalink(pcap_devices_[dev_index].pcap_handle_)); + callback_args.ip_reassembly_ = pcap_devices_ip_reassembly_[dev_index].get(); + + pcap_dispatch(pcap_devices_[dev_index].pcap_handle_, 1, UdpcapSocketPrivate::PacketHandlerRawPtr, reinterpret_cast(&callback_args)); + } if (callback_args.success_) { @@ -419,13 +470,143 @@ namespace Udpcap } else if (wait_result == WAIT_FAILED) { - LOG_DEBUG("Receive error: WAIT_FAILED: " + std::to_string(GetLastError())); + LOG_DEBUG("Receive error: WAIT_FAILED: " + std::system_category().message(GetLastError())); + // TODO: Check if I can always just return here. This definitively happens when I close the socket, so I MUST return in certain cases. But I don't know if there may be cases when this happens without closing the socket. + return {}; } } while (wait_forever || (std::chrono::steady_clock::now() < wait_until)); return 0; } + size_t UdpcapSocketPrivate::receiveDatagram(char* data + , size_t max_len + , unsigned long timeout_ms + , HostAddress* source_address + , uint16_t* source_port + , Udpcap::Error& error) + { + if (!is_valid_) + { + // Invalid socket, cannot bind => fail! + LOG_DEBUG("Receive error: Socket is invalid"); + error = Udpcap::Error::NPCAP_NOT_INITIALIZED; + return 0; + } + + if (!bound_state_) + { + // Not bound => fail! + LOG_DEBUG("Receive error: Socket is not bound"); + error = Udpcap::Error::NOT_BOUND; + return 0; + } + + // Check all devices for data + { + // Variable to store the result + pcap_pkthdr* packet_header (nullptr); + const u_char* packet_data (nullptr); + + // Lock the lists of open pcap devices in read-mode. We may use the handles, but not modify the lists themselfes. + const std::shared_lock pcap_devices_list_lock(pcap_devices_lists_mutex_); + + // Check for data on pcap devices until we are either out of time or have + // received a datagaram. A datagram may consist of multiple packaets in + // case of IP Fragmentation. + while (true) // TODO: respect the timeout parameter + { + bool received_any_data = false; + + { + // Lock the callback lock. While the callback is running, we cannot close the pcap handle, as that may invalidate the data pointer. + const std::lock_guard pcap_devices_callback_lock(pcap_devices_callback_mutex_); + + // Check if the socket is closed and return an error + if (pcap_devices_closed_) + { + error = Udpcap::Error::SOCKET_CLOSED; + return 0; + } + + // Iterate through all devices and check if they have data + for (const auto& pcap_dev : pcap_devices_) + { + CallbackArgsRawPtr callback_args(data, max_len, source_address, source_port, bound_port_, pcpp::LinkLayerType::LINKTYPE_NULL); + + int pcap_next_packet_errorcode = pcap_next_ex(pcap_dev.pcap_handle_, &packet_header, &packet_data); + + if (pcap_next_packet_errorcode == 1) + { + received_any_data = true; + + // Success! + PacketHandlerRawPtr(reinterpret_cast(&callback_args), packet_header, packet_data); + + if (callback_args.success_) + { + // Only return datagram if we successfully received a packet. Otherwise, we will continue receiving data, if there is time left. + error = Udpcap::Error::OK; + return callback_args.bytes_copied_; + } + } + else + { + // TODO: Handle errors coming from pcap. + } + } + } + + // Use WaitForMultipleObjects in order to wait for data on the pcap + // devices. Only wait for data, if we haven't received any data in the + // last loop. The Win32 event will be resetted after we got notified, + // regardless of the amount of packets that are in the buffer. Thus, we + // cannot use the event to always check / wait for new data, as there + // may still be data left in the buffer without the event being set. + if (!received_any_data) + { + // TODO: make WaitForMultipleObjects use the timeout + unsigned long remaining_time_to_wait_ms = INFINITE; + //unsigned long remaining_time_to_wait_ms = 0; + //if (wait_forever) + //{ + // remaining_time_to_wait_ms = INFINITE; + //} + //else + //{ + // auto now = std::chrono::steady_clock::now(); + // if (now < wait_until) + // { + // remaining_time_to_wait_ms = static_cast(std::chrono::duration_cast(wait_until - now).count()); + // } + //} + + + DWORD num_handles = static_cast(pcap_win32_handles_.size()); + if (num_handles > MAXIMUM_WAIT_OBJECTS) + { + LOG_DEBUG("WARNING: Too many open Adapters. " + std::to_string(num_handles) + " adapters are open, only " + std::to_string(MAXIMUM_WAIT_OBJECTS) + " are supported."); + num_handles = MAXIMUM_WAIT_OBJECTS; + } + + const DWORD wait_result = WaitForMultipleObjects(num_handles, pcap_win32_handles_.data(), static_cast(false), remaining_time_to_wait_ms); + + if ((wait_result >= WAIT_OBJECT_0) && wait_result <= (WAIT_OBJECT_0 + num_handles - 1)) + { + // SUCCESS! Some event is notified! We could actually check which + // event it is, in order to read data from that specific event. But + // it is way easier to just let the code above run again and check + // all pcap devices for data. + continue; + } + else + { + // TODO: Handle errors, especially closed and timeout errors + } + } + } + } + } bool UdpcapSocketPrivate::joinMulticastGroup(const HostAddress& group_address) { @@ -463,7 +644,7 @@ namespace Udpcap multicast_groups_.emplace(group_address); // Update the capture filters, so the devices will capture the multicast traffic - updateAllCaptureFilters(); + updateAllCaptureFilters(); // TODO: I probably need to protect the pcap_devices_ list with a mutex here if (multicast_loopback_enabled_) { @@ -499,12 +680,11 @@ namespace Udpcap multicast_groups_.erase(group_it); // Update all capture filtes - updateAllCaptureFilters(); + updateAllCaptureFilters(); // TODO: I probably need to protect the pcap_devices_ list with a mutex here return true; } - void UdpcapSocketPrivate::setMulticastLoopbackEnabled(bool enabled) { if (multicast_loopback_enabled_ == enabled) @@ -521,7 +701,7 @@ namespace Udpcap kickstartLoopbackMulticast(); } - updateAllCaptureFilters(); + updateAllCaptureFilters(); // TODO: I probably need to protect the pcap_devices_ list with a mutex here } bool UdpcapSocketPrivate::isMulticastLoopbackEnabled() const @@ -532,20 +712,47 @@ namespace Udpcap void UdpcapSocketPrivate::close() { // TODO: make close thread safe, so one thread can wait for data while another thread closes the socket - for (auto& pcap_dev : pcap_devices_) + // TODO: 2024-01-30: Check if this now is actually thread safe + { - LOG_DEBUG(std::string("Closing ") + pcap_dev.device_name_); - pcap_close(pcap_dev.pcap_handle_); + // Lock the lists of open pcap devices in read-mode. We may use the handles, + // but not modify the lists themselfes. This is in order to assure that the + // ReceiveDatagram function still has all pcap devices available after + // returning from WaitForMultipleObjects. + const std::shared_lock pcap_devices_lists_lock(pcap_devices_lists_mutex_); + + { + // Lock the callback lock. While the callback is running, we cannot close + // the pcap handle, as that may invalidate the data pointer. + const std::lock_guard pcap_callback_lock(pcap_devices_callback_mutex_); + pcap_devices_closed_ = true; //todo: must i protect this variable with the lists lock or the callback lock + for (auto& pcap_dev : pcap_devices_) + { + LOG_DEBUG(std::string("Closing ") + pcap_dev.device_name_); + pcap_close(pcap_dev.pcap_handle_); + } + } + } + + { + // Lock the lists of open pcap devices in write-mode. We may now modify the lists themselfes. + const std::unique_lock pcap_devices_lists_lock(pcap_devices_lists_mutex_); + pcap_devices_ .clear(); + pcap_win32_handles_ .clear(); + pcap_devices_ip_reassembly_.clear(); } - pcap_devices_ .clear(); - pcap_win32_handles_.clear(); - ip_reassembly_ .clear(); bound_state_ = false; bound_port_ = 0; bound_address_ = HostAddress::Invalid(); } + bool UdpcapSocketPrivate::isClosed() const + { + std::lock_guard pcap_callback_lock(pcap_devices_callback_mutex_); + return pcap_devices_closed_; + } + ////////////////////////////////////////// //// Internal ////////////////////////////////////////// @@ -647,7 +854,7 @@ namespace Udpcap } } - bool UdpcapSocketPrivate::openPcapDevice(const std::string& device_name) + bool UdpcapSocketPrivate::openPcapDevice_nolock(const std::string& device_name) { std::array errbuf{}; @@ -663,12 +870,15 @@ namespace Udpcap pcap_set_promisc(pcap_handle, 1 /*true*/); // We only want Packets destined for this adapter. We are not interested in others. pcap_set_immediate_mode(pcap_handle, 1 /*true*/); + std::array pcap_setnonblock_errbuf{}; + pcap_setnonblock(pcap_handle, 1 /*true*/,pcap_setnonblock_errbuf.data()); + if (receive_buffer_size_ > 0) { - pcap_set_buffer_size(pcap_handle, receive_buffer_size_); + pcap_set_buffer_size(pcap_handle, receive_buffer_size_); // TODO: the buffer size should probably not be zero by default. Currently (2024-01-31) it is. } - const int errorcode = pcap_activate(pcap_handle); + const int errorcode = pcap_activate(pcap_handle); // TODO : If pcap_activate() fails, the pcap_t * is not closed and freed; it should be closed using pcap_close(3PCAP). switch (errorcode) { case 0: @@ -705,9 +915,9 @@ namespace Udpcap const PcapDev pcap_dev(pcap_handle, IsLoopbackDevice(device_name), device_name); - pcap_devices_ .push_back(pcap_dev); - pcap_win32_handles_.push_back(pcap_getevent(pcap_handle)); - ip_reassembly_ .emplace_back(std::make_unique(std::chrono::seconds(5))); + pcap_devices_ .push_back(pcap_dev); + pcap_win32_handles_ .push_back(pcap_getevent(pcap_handle)); + pcap_devices_ip_reassembly_.emplace_back(std::make_unique(std::chrono::seconds(5))); return true; } @@ -788,7 +998,7 @@ namespace Udpcap if (pcap_setfilter(pcap_dev.pcap_handle_, &filter_program) == PCAP_ERROR) { pcap_perror(pcap_dev.pcap_handle_, ("UdpcapSocket ERROR: Unable to set filter \"" + filter_string + "\"").c_str()); - pcap_freecode(&filter_program); + pcap_freecode(&filter_program); // TODO: Check if I need to free the filter program at other places as well (e.g. destructor) } } } @@ -803,7 +1013,7 @@ namespace Udpcap void UdpcapSocketPrivate::kickstartLoopbackMulticast() const { - const uint16_t kickstart_port = 62000; + constexpr uint16_t kickstart_port = 62000; asio::io_context iocontext; asio::ip::udp::socket kickstart_socket(iocontext); @@ -846,6 +1056,7 @@ namespace Udpcap void UdpcapSocketPrivate::PacketHandlerVector(unsigned char* param, const struct pcap_pkthdr* header, const unsigned char* pkt_data) { + std::cerr << "PacketHandlerVector\n"; CallbackArgsVector* callback_args = reinterpret_cast(param); pcpp::RawPacket rawPacket(pkt_data, header->caplen, header->ts, false, callback_args->link_type_); diff --git a/udpcap/src/udpcap_socket_private.h b/udpcap/src/udpcap_socket_private.h index f92cfdc..834ed3e 100644 --- a/udpcap/src/udpcap_socket_private.h +++ b/udpcap/src/udpcap_socket_private.h @@ -20,11 +20,14 @@ #pragma once #include +#include + #include #include #include #include #include +#include #define WIN32_LEAN_AND_MEAN #define NOMINMAX @@ -129,20 +132,28 @@ namespace Udpcap bool isValid() const; bool bind(const HostAddress& local_address, uint16_t local_port); - bool isBound() const; + HostAddress localAddress() const; uint16_t localPort() const; bool setReceiveBufferSize(int buffer_size); + // TODO: Re-implement or remove. This is currently (2024-02-06) implemented faulty. bool hasPendingDatagrams() const; - std::vector receiveDatagram(HostAddress* source_address = nullptr, uint16_t* source_port = nullptr); - std::vector receiveDatagram(unsigned long timeout_ms, HostAddress* source_address = nullptr, uint16_t* source_port = nullptr); + std::vector receiveDatagram_OLD(HostAddress* source_address = nullptr, uint16_t* source_port = nullptr); + std::vector receiveDatagram_OLD(unsigned long timeout_ms, HostAddress* source_address = nullptr, uint16_t* source_port = nullptr); + + size_t receiveDatagram_OLD(char* data, size_t max_len, HostAddress* source_address = nullptr, uint16_t* source_port = nullptr); + size_t receiveDatagram_OLD(char* data, size_t max_len, unsigned long timeout_ms, HostAddress* source_address = nullptr, uint16_t* source_port = nullptr); - size_t receiveDatagram(char* data, size_t max_len, HostAddress* source_address = nullptr, uint16_t* source_port = nullptr); - size_t receiveDatagram(char* data, size_t max_len, unsigned long timeout_ms, HostAddress* source_address = nullptr, uint16_t* source_port = nullptr); + size_t receiveDatagram(char* data + , size_t max_len + , unsigned long timeout_ms + , HostAddress* source_address + , uint16_t* source_port + , Udpcap::Error& error); bool joinMulticastGroup(const HostAddress& group_address); bool leaveMulticastGroup(const HostAddress& group_address); @@ -151,6 +162,7 @@ namespace Udpcap bool isMulticastLoopbackEnabled() const; void close(); + bool isClosed() const; ////////////////////////////////////////// //// Internal @@ -164,7 +176,7 @@ namespace Udpcap static std::string getMac(pcap_t* const pcap_handle); - bool openPcapDevice(const std::string& device_name); + bool openPcapDevice_nolock(const std::string& device_name); std::string createFilterString(PcapDev& pcap_dev) const; void updateCaptureFilter(PcapDev& pcap_dev); @@ -189,9 +201,12 @@ namespace Udpcap std::set multicast_groups_; bool multicast_loopback_enabled_; /**< Winsocks style IP_MULTICAST_LOOP: if enabled, the socket can receive loopback multicast packages */ + mutable std::shared_mutex pcap_devices_lists_mutex_; /**< Mutex to protect the pcap_devices_, pcap_win32_handles_, pcap_devices_ip_reassembly_ lists. Only the lists, not the content. */ + mutable std::mutex pcap_devices_callback_mutex_; /**< Mutex to protect the pcap_devices during a callback AND the pcap_devices_closed variable. While a callback is running, the pcap_devices MUST NOT be closed. */ + bool pcap_devices_closed_; /**< Tells whether we have already closed the socket. */ std::vector pcap_devices_; /**< List of open PcapDevices */ std::vector pcap_win32_handles_; /**< Native Win32 handles to wait for data on the PCAP Devices. The List is in sync with pcap_devices. */ - std::vector> ip_reassembly_; /**< IP Reassembly for fragmented IP traffic. The list is in sync with the pcap_devices. */ + std::vector> pcap_devices_ip_reassembly_; /**< IP Reassembly for fragmented IP traffic. The list is in sync with the pcap_devices. */ int receive_buffer_size_; };