initial commit
This commit is contained in:
commit
811f80c92c
17 changed files with 549 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
.idea
|
||||
cmake-build-*
|
6
.gitmodules
vendored
Normal file
6
.gitmodules
vendored
Normal file
|
@ -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
|
28
CMakeLists.txt
Normal file
28
CMakeLists.txt
Normal file
|
@ -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
|
||||
)
|
19
README.md
Normal file
19
README.md
Normal file
|
@ -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 ...
|
||||
```
|
1
external/cpr
vendored
Submodule
1
external/cpr
vendored
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit c44f8d57b00afd9d6d2a85cb85fa97081f30692b
|
1
external/json
vendored
Submodule
1
external/json
vendored
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit 663058e7d18241338aec42846d4f77995275ccf6
|
41
source/gns3/GnsNode.cpp
Normal file
41
source/gns3/GnsNode.cpp
Normal file
|
@ -0,0 +1,41 @@
|
|||
#include "GnsNode.hpp"
|
||||
#include "GnsPort.hpp"
|
||||
#include "GnsProject.hpp"
|
||||
|
||||
#include <cpr/cpr.h>
|
||||
#include <vector>
|
||||
|
||||
|
||||
namespace gns {
|
||||
|
||||
|
||||
GnsNode::GnsNode(const GnsProject *project, std::string uuid, const std::vector<GnsPort>& 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<GnsPort> 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);
|
||||
}
|
||||
|
||||
|
||||
}
|
53
source/gns3/GnsNode.hpp
Normal file
53
source/gns3/GnsNode.hpp
Normal file
|
@ -0,0 +1,53 @@
|
|||
#pragma once
|
||||
|
||||
#include "GnsPort.hpp"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
|
||||
|
||||
namespace gns {
|
||||
|
||||
|
||||
class GnsProject;
|
||||
|
||||
|
||||
/**
|
||||
* Represent a GNS3 node
|
||||
*/
|
||||
class GnsNode {
|
||||
public:
|
||||
GnsNode(const GnsProject* project, std::string uuid, const std::vector<GnsPort>& 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<GnsPort> getPorts() const;
|
||||
|
||||
/**
|
||||
* Start the node
|
||||
*/
|
||||
void start() const;
|
||||
|
||||
private:
|
||||
const GnsProject* project;
|
||||
std::string uuid;
|
||||
std::vector<GnsPort> ports;
|
||||
};
|
||||
|
||||
|
||||
}
|
12
source/gns3/GnsPort.cpp
Normal file
12
source/gns3/GnsPort.cpp
Normal file
|
@ -0,0 +1,12 @@
|
|||
#include "GnsPort.hpp"
|
||||
|
||||
|
||||
namespace gns {
|
||||
|
||||
|
||||
GnsPort::GnsPort(const std::optional<std::string>& mac_address) {
|
||||
this->mac_address = mac_address;
|
||||
}
|
||||
|
||||
|
||||
}
|
20
source/gns3/GnsPort.hpp
Normal file
20
source/gns3/GnsPort.hpp
Normal file
|
@ -0,0 +1,20 @@
|
|||
#pragma once
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
|
||||
namespace gns {
|
||||
|
||||
|
||||
/**
|
||||
* Represent a port of a GNS3 node
|
||||
*/
|
||||
class GnsPort {
|
||||
public:
|
||||
explicit GnsPort(const std::optional<std::string>& mac_address);
|
||||
|
||||
std::optional<std::string> mac_address;
|
||||
};
|
||||
|
||||
|
||||
}
|
74
source/gns3/GnsProject.cpp
Normal file
74
source/gns3/GnsProject.cpp
Normal file
|
@ -0,0 +1,74 @@
|
|||
#include "GnsProject.hpp"
|
||||
#include "GnsServer.hpp"
|
||||
#include "GnsPort.hpp"
|
||||
|
||||
#include <cpr/cpr.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
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<GnsNode> GnsProject::getNodes() const {
|
||||
std::vector<GnsNode> 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<json> ports_data = node_data.value("ports", std::vector<json>{});
|
||||
std::vector<GnsPort> ports;
|
||||
ports.reserve(ports_data.size());
|
||||
for (const auto& port_data : ports_data) {
|
||||
// get the mac of the port
|
||||
std::optional<std::string> 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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
45
source/gns3/GnsProject.hpp
Normal file
45
source/gns3/GnsProject.hpp
Normal file
|
@ -0,0 +1,45 @@
|
|||
#pragma once
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#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<GnsNode> getNodes() const;
|
||||
|
||||
private:
|
||||
const GnsServer* server;
|
||||
std::string uuid;
|
||||
};
|
||||
|
||||
|
||||
}
|
62
source/gns3/GnsServer.cpp
Normal file
62
source/gns3/GnsServer.cpp
Normal file
|
@ -0,0 +1,62 @@
|
|||
#include "GnsServer.hpp"
|
||||
|
||||
#include <cpr/cpr.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
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<GnsProject> GnsServer::getProjects() const {
|
||||
std::vector<GnsProject> 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<GnsNode> GnsServer::getNodes() const {
|
||||
std::vector<GnsNode> nodes;
|
||||
|
||||
// get all the nodes from all the projects
|
||||
for (const GnsProject& project : this->getProjects()) {
|
||||
std::vector<GnsNode> project_nodes = project.getNodes();
|
||||
nodes.insert(nodes.end(), project_nodes.begin(), project_nodes.end());
|
||||
}
|
||||
|
||||
return nodes;
|
||||
}
|
||||
|
||||
|
||||
}
|
44
source/gns3/GnsServer.hpp
Normal file
44
source/gns3/GnsServer.hpp
Normal file
|
@ -0,0 +1,44 @@
|
|||
#pragma once
|
||||
#include <string>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#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<GnsProject> getProjects() const;
|
||||
|
||||
/**
|
||||
* Get all the nodes in the server
|
||||
* @return all the nodes in the server
|
||||
*/
|
||||
[[nodiscard]] std::vector<GnsNode> getNodes() const;
|
||||
|
||||
private:
|
||||
std::string host;
|
||||
std::uint16_t port;
|
||||
};
|
||||
|
||||
|
||||
}
|
100
source/main.cpp
Normal file
100
source/main.cpp
Normal file
|
@ -0,0 +1,100 @@
|
|||
#include <cstring>
|
||||
#include <functional>
|
||||
#include <iomanip>
|
||||
|
||||
#include "gns3/GnsPort.hpp"
|
||||
#include "gns3/GnsServer.hpp"
|
||||
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <pcap.h>
|
||||
#include <netinet/ether.h>
|
||||
#include <netinet/udp.h>
|
||||
|
||||
#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<gns::GnsServer*>(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<const ether_header*>(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_t, decltype(&pcap_close)>(
|
||||
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<uint8_t*>(&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;
|
||||
}
|
28
source/utils/address.cpp
Normal file
28
source/utils/address.cpp
Normal file
|
@ -0,0 +1,28 @@
|
|||
#include "address.hpp"
|
||||
|
||||
#include <iomanip>
|
||||
#include <ios>
|
||||
|
||||
|
||||
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<int>(address_bytes[i]);
|
||||
// unless this is the last byte, add a separator
|
||||
if (i < MAC_ADDRESS_LENGTH - 1) stream << ":";
|
||||
}
|
||||
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
|
||||
}
|
13
source/utils/address.hpp
Normal file
13
source/utils/address.hpp
Normal file
|
@ -0,0 +1,13 @@
|
|||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <cstdint>
|
||||
|
||||
|
||||
namespace utils::address {
|
||||
|
||||
|
||||
std::string mac_bytes_to_string(const std::uint8_t address_bytes[6]);
|
||||
|
||||
|
||||
}
|
Loading…
Reference in a new issue