From 811f80c92c09a3785caa4fb05daeaf6a6fb4e87d Mon Sep 17 00:00:00 2001 From: faraphel Date: Sun, 22 Dec 2024 23:05:52 +0100 Subject: [PATCH] initial commit --- .gitignore | 2 + .gitmodules | 6 +++ CMakeLists.txt | 28 +++++++++++ README.md | 19 +++++++ external/cpr | 1 + external/json | 1 + source/gns3/GnsNode.cpp | 41 +++++++++++++++ source/gns3/GnsNode.hpp | 53 ++++++++++++++++++++ source/gns3/GnsPort.cpp | 12 +++++ source/gns3/GnsPort.hpp | 20 ++++++++ source/gns3/GnsProject.cpp | 74 +++++++++++++++++++++++++++ source/gns3/GnsProject.hpp | 45 +++++++++++++++++ source/gns3/GnsServer.cpp | 62 +++++++++++++++++++++++ source/gns3/GnsServer.hpp | 44 ++++++++++++++++ source/main.cpp | 100 +++++++++++++++++++++++++++++++++++++ source/utils/address.cpp | 28 +++++++++++ source/utils/address.hpp | 13 +++++ 17 files changed, 549 insertions(+) create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 CMakeLists.txt create mode 100644 README.md create mode 160000 external/cpr create mode 160000 external/json create mode 100644 source/gns3/GnsNode.cpp create mode 100644 source/gns3/GnsNode.hpp create mode 100644 source/gns3/GnsPort.cpp create mode 100644 source/gns3/GnsPort.hpp create mode 100644 source/gns3/GnsProject.cpp create mode 100644 source/gns3/GnsProject.hpp create mode 100644 source/gns3/GnsServer.cpp create mode 100644 source/gns3/GnsServer.hpp create mode 100644 source/main.cpp create mode 100644 source/utils/address.cpp create mode 100644 source/utils/address.hpp diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1c85d0e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea +cmake-build-* diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..c964e0f --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "external/json"] + path = external/json + url = https://github.com/nlohmann/json +[submodule "external/cpr"] + path = external/cpr + url = https://github.com/libcpr/cpr diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..e0b73e1 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,28 @@ +cmake_minimum_required(VERSION 3.29) +project(gns3_wol_emulator) + +set(CMAKE_CXX_STANDARD 23) + +# Dependencies +add_subdirectory(external/json) +add_subdirectory(external/cpr) + +# Project +add_executable(gns3_wol_emulator + source/main.cpp + source/gns3/GnsServer.cpp + source/gns3/GnsServer.hpp + source/gns3/GnsProject.cpp + source/gns3/GnsProject.hpp + source/gns3/GnsNode.cpp + source/gns3/GnsNode.hpp + source/gns3/GnsPort.cpp + source/gns3/GnsPort.hpp + source/utils/address.cpp + source/utils/address.hpp +) +target_link_libraries(gns3_wol_emulator PRIVATE + nlohmann_json::nlohmann_json + cpr::cpr + pcap +) diff --git a/README.md b/README.md new file mode 100644 index 0000000..d5d5545 --- /dev/null +++ b/README.md @@ -0,0 +1,19 @@ +# GNS3 WoL Emulator + +Emulate Wake-On-LAN behavior in a GNS3 environment. + +This run on the host GNS3 server and listen for a WoL message on the system on any device. +If found, it will wake up the destination machine based on its MAC address. + +## Dependencies + +Debian : +``` +sudo apt install libpcap-dev +``` + +## Build + +``` +cmake ... +``` \ No newline at end of file diff --git a/external/cpr b/external/cpr new file mode 160000 index 0000000..c44f8d5 --- /dev/null +++ b/external/cpr @@ -0,0 +1 @@ +Subproject commit c44f8d57b00afd9d6d2a85cb85fa97081f30692b diff --git a/external/json b/external/json new file mode 160000 index 0000000..663058e --- /dev/null +++ b/external/json @@ -0,0 +1 @@ +Subproject commit 663058e7d18241338aec42846d4f77995275ccf6 diff --git a/source/gns3/GnsNode.cpp b/source/gns3/GnsNode.cpp new file mode 100644 index 0000000..e78fd5b --- /dev/null +++ b/source/gns3/GnsNode.cpp @@ -0,0 +1,41 @@ +#include "GnsNode.hpp" +#include "GnsPort.hpp" +#include "GnsProject.hpp" + +#include +#include + + +namespace gns { + + +GnsNode::GnsNode(const GnsProject *project, std::string uuid, const std::vector& ports) { + this->project = project; + this->uuid = std::move(uuid); + this->ports = ports; +} + +std::string GnsNode::getApiBase() const { + return this->project->getApiBase() + "nodes/" + this->uuid; +} + +std::string GnsNode::getUuid() const { + return this->uuid; +} + +std::vector GnsNode::getPorts() const { + return this->ports; +} + +void GnsNode::start() const { + // request the API endpoint + cpr::Url endpoint = this->getApiBase() + "start"; + cpr::Response response = Post(endpoint); + + // check for a valid response + if (response.error.code != cpr::ErrorCode::OK) + throw std::runtime_error(response.error.message); +} + + +} diff --git a/source/gns3/GnsNode.hpp b/source/gns3/GnsNode.hpp new file mode 100644 index 0000000..d6303a5 --- /dev/null +++ b/source/gns3/GnsNode.hpp @@ -0,0 +1,53 @@ +#pragma once + +#include "GnsPort.hpp" + +#include +#include + + + +namespace gns { + + +class GnsProject; + + +/** +* Represent a GNS3 node +*/ +class GnsNode { +public: + GnsNode(const GnsProject* project, std::string uuid, const std::vector& ports); + + /** + * Get the base URL for the node API + * @return the base URL for the node API + */ + [[nodiscard]] std::string getApiBase() const; + + /** + * Get the uuid of the node + * @return the uuid of the node + */ + [[nodiscard]] std::string getUuid() const; + + /** + * Get the ports of the node + * @return the ports of the node + */ + [[nodiscard]] std::vector getPorts() const; + + /** + * Start the node + */ + void start() const; + +private: + const GnsProject* project; + std::string uuid; + std::vector ports; +}; + + +} diff --git a/source/gns3/GnsPort.cpp b/source/gns3/GnsPort.cpp new file mode 100644 index 0000000..511d4ad --- /dev/null +++ b/source/gns3/GnsPort.cpp @@ -0,0 +1,12 @@ +#include "GnsPort.hpp" + + +namespace gns { + + +GnsPort::GnsPort(const std::optional& mac_address) { + this->mac_address = mac_address; +} + + +} \ No newline at end of file diff --git a/source/gns3/GnsPort.hpp b/source/gns3/GnsPort.hpp new file mode 100644 index 0000000..6a59b1c --- /dev/null +++ b/source/gns3/GnsPort.hpp @@ -0,0 +1,20 @@ +#pragma once +#include +#include + + +namespace gns { + + +/** + * Represent a port of a GNS3 node + */ +class GnsPort { +public: + explicit GnsPort(const std::optional& mac_address); + + std::optional mac_address; +}; + + +} diff --git a/source/gns3/GnsProject.cpp b/source/gns3/GnsProject.cpp new file mode 100644 index 0000000..dec21b7 --- /dev/null +++ b/source/gns3/GnsProject.cpp @@ -0,0 +1,74 @@ +#include "GnsProject.hpp" +#include "GnsServer.hpp" +#include "GnsPort.hpp" + +#include +#include +using json = nlohmann::json; + + +namespace gns { + + +GnsProject::GnsProject(const GnsServer* server, std::string uuid) { + this->server = server; + this->uuid = std::move(uuid); +} + +std::string GnsProject::getApiBase() const { + return this->server->getApiBase() + "projects/" + this->uuid + "/"; +} + +std::string GnsProject::getUuid() const { + return this->uuid; +} + +std::vector GnsProject::getNodes() const { + std::vector nodes; + + // request the API endpoint + cpr::Url endpoint = this->getApiBase() + "nodes"; + cpr::Response response = Get(endpoint); + + // check for a valid response + if (response.error.code != cpr::ErrorCode::OK) + throw std::runtime_error(response.error.message); + + // check for valid content + if (response.header["Content-Type"] != "application/json") + throw std::runtime_error("Data must be JSON formatted."); + + // parse the data + json data = json::parse(response.text); + + // deserialize the projects + nodes.reserve(data.size()); + for (const auto& node_data : data) { + // parse the ports of the node + std::vector ports_data = node_data.value("ports", std::vector{}); + std::vector ports; + ports.reserve(ports_data.size()); + for (const auto& port_data : ports_data) { + // get the mac of the port + std::optional mac_address; + if (port_data.contains("mac_address")) + mac_address.emplace(port_data["mac_address"]); + + // save the port data + ports.emplace_back(mac_address); + } + + // save the node + nodes.emplace_back( + this, + node_data["node_id"], + ports + ); + } + + return nodes; +} + + + +} diff --git a/source/gns3/GnsProject.hpp b/source/gns3/GnsProject.hpp new file mode 100644 index 0000000..9f67922 --- /dev/null +++ b/source/gns3/GnsProject.hpp @@ -0,0 +1,45 @@ +#pragma once +#include +#include + +#include "GnsNode.hpp" + + +namespace gns { + + +class GnsServer; + + +/** + * Represent a GNS3 project. + */ +class GnsProject { +public: + GnsProject(const GnsServer* server, std::string uuid); + + /** + * Get the base prefix for an API request + * @return the base prefix for an API request + */ + [[nodiscard]] std::string getApiBase() const; + + /** + * Get the uuid of the project + * @return the uuid of the project + */ + [[nodiscard]] std::string getUuid() const; + + /** + * Get all the nodes of the project + * @return all the nodes of the project + */ + [[nodiscard]] std::vector getNodes() const; + +private: + const GnsServer* server; + std::string uuid; +}; + + +} diff --git a/source/gns3/GnsServer.cpp b/source/gns3/GnsServer.cpp new file mode 100644 index 0000000..566a729 --- /dev/null +++ b/source/gns3/GnsServer.cpp @@ -0,0 +1,62 @@ +#include "GnsServer.hpp" + +#include +#include +using json = nlohmann::json; + + +namespace gns { + + +GnsServer::GnsServer(const std::string &host, const std::uint16_t port) { + this->host = host; + this->port = port; +} + +std::string GnsServer::getApiBase() const { + return "http://" + this->host + ":" + std::to_string(this->port) + "/v2/"; +} + +std::vector GnsServer::getProjects() const { + std::vector projects; + + // request the API endpoint + cpr::Url endpoint = this->getApiBase() + "projects"; + cpr::Response response = Get(endpoint); + + // check for a valid response + if (response.error.code != cpr::ErrorCode::OK) + throw std::runtime_error(response.error.message); + + // check for valid content + if (response.header["Content-Type"] != "application/json") + throw std::runtime_error("Data must be JSON formatted."); + + // parse the data + json data = json::parse(response.text); + + // deserialize the projects + projects.reserve(data.size()); + for (const auto& project_data : data) + projects.emplace_back( + this, + project_data["project_id"] + ); + + return projects; +} + +std::vector GnsServer::getNodes() const { + std::vector nodes; + + // get all the nodes from all the projects + for (const GnsProject& project : this->getProjects()) { + std::vector project_nodes = project.getNodes(); + nodes.insert(nodes.end(), project_nodes.begin(), project_nodes.end()); + } + + return nodes; +} + + +} diff --git a/source/gns3/GnsServer.hpp b/source/gns3/GnsServer.hpp new file mode 100644 index 0000000..be95bf1 --- /dev/null +++ b/source/gns3/GnsServer.hpp @@ -0,0 +1,44 @@ +#pragma once +#include +#include +#include + +#include "GnsProject.hpp" + + +namespace gns { + + +/** + * Represent a GNS3 server + * Wrapper around the GNS3 API (https://gns3-server.readthedocs.io/en/latest/endpoints.html) + */ +class GnsServer { +public: + GnsServer(const std::string& host, std::uint16_t port); + + /** + * Get the base prefix for an API request + * @return the base prefix for an API request + */ + [[nodiscard]] std::string getApiBase() const; + + /** + * Get all the projects in the server + * @return all the projects in the server + */ + [[nodiscard]] std::vector getProjects() const; + + /** + * Get all the nodes in the server + * @return all the nodes in the server + */ + [[nodiscard]] std::vector getNodes() const; + +private: + std::string host; + std::uint16_t port; +}; + + +} diff --git a/source/main.cpp b/source/main.cpp new file mode 100644 index 0000000..b0f4002 --- /dev/null +++ b/source/main.cpp @@ -0,0 +1,100 @@ +#include +#include +#include + +#include "gns3/GnsPort.hpp" +#include "gns3/GnsServer.hpp" + +#include +#include +#include +#include +#include + +#include "utils/address.hpp" + + +/** + * Callback of a detected Wake-On-LAN packet + */ +void packet_wol_handler( + unsigned char *user_data, + const pcap_pkthdr* header, + const std::uint8_t* packet +) { + const auto server = reinterpret_cast(user_data); + + // get the special sll header (added by pcap when using the "any" device) + std::uint16_t sll_header = *packet; + // get the ethernet header of the packet + auto* packet_ethernet_header = reinterpret_cast(packet + sizeof(sll_header)); + // get the content of the packet + const std::uint8_t* packet_content_start = packet + sizeof(sll_header) + sizeof(ether_header); + const std::vector packet_content(packet_content_start, packet_content_start + header->len); + + std::cout << "Captured a WoL packet." << std::endl; + std::cout << "Packet length: " + std::to_string(header->len) + " bytes" << std::endl; + + // get the source address + const std::string mac_address_source = utils::address::mac_bytes_to_string(packet_ethernet_header->ether_shost); + std::cout << "Source: " << mac_address_source << std::endl; + + // TODO(Faraphel): check the magic header ? content[6:12] + + // get the destination address + const std::string mac_address_target = utils::address::mac_bytes_to_string(packet_content.data() + 6); + std::cout << "Destination: " << mac_address_target << std::endl; + + // TODO(Faraphel): check the 16 repetitions of the source address ? + + // find the machine with the mac address + for (const gns::GnsNode& node : server->getNodes()) + for (const gns::GnsPort& port : node.getPorts()) + if (port.mac_address == mac_address_target) { + std::cout << "Matching node: " + node.getUuid() << std::endl; + node.start(); + std::cout << "Node started." << std::endl; + return; + } + + std::cerr << "Found no matching node." << std::endl +} + +int main() { + // get the GNS3 server + auto server = gns::GnsServer("localhost", 80); + + // capture any packet on the selected interface + char error_buffer[PCAP_ERRBUF_SIZE]; + const auto handle = std::unique_ptr( + pcap_open_live("any", BUFSIZ, 1, 1000, error_buffer), // capture on all interface + pcap_close + ); + + if (handle == nullptr) + throw std::runtime_error("pcap_open_live() failed: " + std::string(error_buffer)); + + // compile the packet filter + bpf_program filter {}; + const std::string filter_expression = "ether proto 0x0842"; // only match WoL packets + if (pcap_compile(handle.get(), &filter, filter_expression.c_str(), 0, PCAP_NETMASK_UNKNOWN) == -1) + throw std::runtime_error("pcap_compile() failed: " + std::string(error_buffer)); + + // apply the filter + if (pcap_setfilter(handle.get(), &filter) == -1) + throw std::runtime_error("pcap_setfilter() failed: " + std::string(error_buffer)); + + // Start packet capture loop + if (pcap_loop( + handle.get(), + 0, + packet_wol_handler, + reinterpret_cast(&server) + ) == -1) + throw std::runtime_error("pcap_loop() failed: " + std::string(error_buffer)); + + // TODO(Faraphel): more forgiving exception handling + // TODO(Faraphel): if possible, check if the two ports are in the same network. + + return 0; +} diff --git a/source/utils/address.cpp b/source/utils/address.cpp new file mode 100644 index 0000000..eaea9d3 --- /dev/null +++ b/source/utils/address.cpp @@ -0,0 +1,28 @@ +#include "address.hpp" + +#include +#include + + +namespace utils::address { + + +constexpr size_t MAC_ADDRESS_LENGTH = 6; + + +std::string mac_bytes_to_string(const std::uint8_t address_bytes[MAC_ADDRESS_LENGTH]) { + std::stringstream stream; + + // write the mac address as a string + for (std::size_t i = 0; i < MAC_ADDRESS_LENGTH; i++) { + // format the byte + stream << std::hex << std::setw(2) << std::setfill('0') << static_cast(address_bytes[i]); + // unless this is the last byte, add a separator + if (i < MAC_ADDRESS_LENGTH - 1) stream << ":"; + } + + return stream.str(); +} + + +} diff --git a/source/utils/address.hpp b/source/utils/address.hpp new file mode 100644 index 0000000..5c9cc87 --- /dev/null +++ b/source/utils/address.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include +#include + + +namespace utils::address { + + +std::string mac_bytes_to_string(const std::uint8_t address_bytes[6]); + + +}