diff --git a/.gitignore b/.gitignore index 7fa6f2e..c6c928b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ build .DS_Store .idea compile_commands.json +config/ \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index c8b42c8..beacbdd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -41,6 +41,8 @@ include_directories(${ProtobufIncludePath}) SET(VNET_SOURCE_FILES src/common/socket_utils.cpp + src/common/tun.cpp + src/protocol/header.cpp src/protocol/types.cpp src/protocol/dispatch.cpp @@ -72,11 +74,14 @@ add_subdirectory(src/agent) ######################## GOOGLE TEST ######################## ############################################################# -include(FetchContent) -FetchContent_Declare( - googletest - URL https://github.com/google/googletest/archive/refs/tags/v1.17.0.zip -) -FetchContent_MakeAvailable(googletest) +find_package(GTest QUIET) +if (NOT GTest_FOUND) + include(FetchContent) + FetchContent_Declare( + googletest + URL https://github.com/google/googletest/archive/refs/tags/v1.17.0.zip + ) + FetchContent_MakeAvailable(googletest) +endif() enable_testing() add_subdirectory(tests) diff --git a/docker/Dockerfile.agent b/docker/Dockerfile.agent index 2c73629..7dd20c6 100644 --- a/docker/Dockerfile.agent +++ b/docker/Dockerfile.agent @@ -5,6 +5,8 @@ RUN apt-get update && apt-get install -y \ cmake \ protobuf-compiler \ libprotobuf-dev \ + libgtest-dev \ + libgmock-dev \ && rm -rf /var/lib/apt/lists/* WORKDIR /app @@ -16,7 +18,7 @@ RUN cmake -S . -B build -DCMAKE_BUILD_TYPE=Release && \ FROM ubuntu:24.04 RUN apt-get update && apt-get install -y --no-install-recommends \ - libprotobuf32t64 bash \ + libprotobuf32t64 bash iproute2 \ && rm -rf /var/lib/apt/lists/* COPY --from=builder /app/build/agent /usr/local/bin/agent diff --git a/docker/Dockerfile.conductor b/docker/Dockerfile.conductor index 8a9b3f2..9c8939b 100644 --- a/docker/Dockerfile.conductor +++ b/docker/Dockerfile.conductor @@ -5,6 +5,8 @@ RUN apt-get update && apt-get install -y \ cmake \ protobuf-compiler \ libprotobuf-dev \ + libgtest-dev \ + libgmock-dev \ && rm -rf /var/lib/apt/lists/* WORKDIR /app diff --git a/docker/Dockerfile.switch b/docker/Dockerfile.switch index 8be1583..9c06730 100644 --- a/docker/Dockerfile.switch +++ b/docker/Dockerfile.switch @@ -5,6 +5,8 @@ RUN apt-get update && apt-get install -y \ cmake \ protobuf-compiler \ libprotobuf-dev \ + libgtest-dev \ + libgmock-dev \ && rm -rf /var/lib/apt/lists/* WORKDIR /app @@ -16,9 +18,12 @@ RUN cmake -S . -B build -DCMAKE_BUILD_TYPE=Release && \ FROM ubuntu:24.04 RUN apt-get update && apt-get install -y --no-install-recommends \ - libprotobuf32t64 bash \ + libprotobuf32t64 bash iproute2 \ && rm -rf /var/lib/apt/lists/* COPY --from=builder /app/build/switch /usr/local/bin/switch +COPY docker/switch-entrypoint.sh /usr/local/bin/switch-entrypoint.sh +RUN chmod +x /usr/local/bin/switch-entrypoint.sh +ENTRYPOINT [ "/usr/local/bin/switch-entrypoint.sh" ] CMD ["/usr/local/bin/switch"] diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 8dadda5..2ca4c08 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -30,6 +30,13 @@ services: environment: - CONDUCTOR_IP=vnet-conductor - CONDUCTOR_PORT=5000 + - VNET_CONFIG_PATH=/etc/vnet/config.txt + volumes: + - ../config/switch1.txt:/etc/vnet/config.txt:ro + cap_add: + - NET_ADMIN + devices: + - /dev/net/tun:/dev/net/tun command: ["/usr/local/bin/switch", "switch1", "authkey1", "6000"] healthcheck: test: ["CMD", "bash", "-c", "timeout 1 bash -c 'echo > /dev/tcp/127.0.0.1/6000' || exit 1"] @@ -55,6 +62,13 @@ services: environment: - CONDUCTOR_IP=vnet-conductor - CONDUCTOR_PORT=5000 + - VNET_CONFIG_PATH=/etc/vnet/config.txt + volumes: + - ../config/switch2.txt:/etc/vnet/config.txt:ro + cap_add: + - NET_ADMIN + devices: + - /dev/net/tun:/dev/net/tun command: ["/usr/local/bin/switch", "switch2", "authkey2", "6001"] healthcheck: test: ["CMD", "bash", "-c", "timeout 1 bash -c 'echo > /dev/tcp/127.0.0.1/6001' || exit 1"] @@ -80,6 +94,13 @@ services: environment: - CONDUCTOR_IP=vnet-conductor - CONDUCTOR_PORT=5000 + - VNET_CONFIG_PATH=/etc/vnet/config.txt + volumes: + - ../config/switch3.txt:/etc/vnet/config.txt:ro + cap_add: + - NET_ADMIN + devices: + - /dev/net/tun:/dev/net/tun command: ["/usr/local/bin/switch", "switch3", "authkey3", "6002"] healthcheck: test: ["CMD", "bash", "-c", "timeout 1 bash -c 'echo > /dev/tcp/127.0.0.1/6002' || exit 1"] @@ -109,6 +130,10 @@ services: environment: - CONDUCTOR_IP=vnet-conductor - CONDUCTOR_PORT=5000 + cap_add: + - NET_ADMIN + devices: + - /dev/net/tun:/dev/net/tun command: ["/usr/local/bin/agent", "agent1", "agentkey1"] logging: driver: "json-file" diff --git a/docker/switch-entrypoint.sh b/docker/switch-entrypoint.sh new file mode 100755 index 0000000..3986549 --- /dev/null +++ b/docker/switch-entrypoint.sh @@ -0,0 +1,9 @@ +#!/bin/bash +set -e + +if [ ! -f "${VNET_CONFIG_PATH:-/etc/vnet/config.txt}" ]; then + echo "[Switch] ERROR: config file not found or is a directory: ${VNET_CONFIG_PATH:-/etc/vnet/config.txt}" + exit 1 +fi + +exec "$@" diff --git a/include/common/tun.h b/include/common/tun.h new file mode 100644 index 0000000..2503bb1 --- /dev/null +++ b/include/common/tun.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#include + +/** + * @brief Open a TUN device and configure it. + * + * Creates a TUN interface with the given name, assigns the given + * IPv4 address with a /24 prefix, and brings the interface up. + * + * @param name Interface name (e.g. "vnet0"). Max 15 chars. + * @param ipv4 Virtual IPv4 address in network order. + * @param prefix_len Subnet prefix length (default 24 = /24). + * @return File descriptor for the TUN device, or -1 on failure. + */ +int tun_open(const std::string& name, uint32_t ipv4, uint8_t prefix_len = 24); + +/** + * @brief Close a TUN device and bring the interface down. + */ +void tun_close(int fd, const std::string& name); diff --git a/include/vnet/protocol/dispatch.hpp b/include/vnet/protocol/dispatch.hpp index bbe73aa..abe7b4d 100644 --- a/include/vnet/protocol/dispatch.hpp +++ b/include/vnet/protocol/dispatch.hpp @@ -26,6 +26,11 @@ namespace vnet::protocol { /** Reconnection */ virtual void onAgentMRP (netqueue::socket_data data, mip::PacketAgentMRP &packet); + /** Routing */ + virtual void onPrepareRouteForTarget(netqueue::socket_data data, mip::PacketPrepareRouteForTarget &packet); + virtual void onNextForTarget(netqueue::socket_data data, mip::PacketNextForTarget &packet); + virtual void onIPv4Raw(netqueue::socket_data data, mip::PacketIPv4Raw &packet); + virtual ~Dispatch() = default; }; diff --git a/include/vnet/protocol/types.hpp b/include/vnet/protocol/types.hpp index af3a2cc..e50cd2f 100644 --- a/include/vnet/protocol/types.hpp +++ b/include/vnet/protocol/types.hpp @@ -8,6 +8,7 @@ namespace vnet::protocol { enum PacketType : uint16_t { HEARTBEAT = 0, + // MIP SWITCH_MIP, AGENT_MIP, @@ -16,7 +17,13 @@ namespace vnet::protocol { AUTH_CONNECT_TO_SWITCH, CONNECTION_ACCEPTED, - AGENT_MRP + // Reconnection + AGENT_MRP, + + // Routing + PREPARE_ROUTE_FOR_TARGET, + NEXT_FOR_TARGET, + IPV4_RAW }; PacketType ntoh_packet_type (uint_packet_t type); diff --git a/proto/mip.proto b/proto/mip.proto index c32f93c..8bc2ff1 100644 --- a/proto/mip.proto +++ b/proto/mip.proto @@ -34,9 +34,29 @@ message PacketAuthConnectToSwitch { fixed64 connection_token = 1; } -message PacketConnectionAccepted {} +message PacketConnectionAccepted { + fixed32 virtual_ipv4 = 1; +} message PacketAgentMRP { string name = 1; string auth_key = 2; } + +message PacketPrepareRouteForTarget { + fixed32 dest_ipv4 = 1; +} + +message PacketNextForTarget { + fixed32 dest_ipv4 = 1; + + // next_switch_ipv4 = 0 means the target disconnected, remove the route + fixed32 next_switch_ipv4 = 2; + + uint32 next_switch_port = 3; + string next_switch_name = 4; +} + +message PacketIPv4Raw { + bytes payload = 1; +} diff --git a/src/agent/setup.cpp b/src/agent/setup.cpp index 33c45a6..944646f 100644 --- a/src/agent/setup.cpp +++ b/src/agent/setup.cpp @@ -10,6 +10,7 @@ #include #include +#include "common/tun.h" #include "mip.pb.h" #include "common/config.h" #include "common/socket_utils.h" @@ -206,11 +207,25 @@ int main(int argc, char** argv) { return 1; } - std::cout << "[Agent] Authenticated with switch " - << assignment.switch_name() << "!\n"; + uint32_t virtual_ipv4 = ack.virtual_ipv4(); + std::cout << "[Agent] Authenticated with switch " << assignment.switch_name() + << ", virtual IP: " << ipv4_to_string(virtual_ipv4) << "\n"; // =================================================================== - // STEP 4 — Switch both fds to non-blocking, enter event loop + // STEP 4 — Open TUN device with assigned virtual IP + // =================================================================== + + std::string tun_name = "vnet-" + agent_name; + int tun_fd = tun_open(tun_name, virtual_ipv4); + if (tun_fd < 0) { + std::cerr << "[Agent] Failed to open TUN device\n"; + close(sw_sock); + close(cdt_sock); + return 1; + } + + // =================================================================== + // STEP 5 — Switch both fds to non-blocking, enter event loop // =================================================================== set_nonblocking(cdt_sock); set_nonblocking(sw_sock); @@ -235,6 +250,8 @@ int main(int argc, char** argv) { g_state.sw = sw_info; queue.put_sck(sw_sock, sw_info); + queue.put_tun(tun_fd, nullptr); + std::cout << "[Agent] Entering event loop.\n"; while (g_running) { @@ -243,6 +260,7 @@ int main(int argc, char** argv) { } std::cout << "[Agent] Shutting down.\n"; + tun_close(tun_fd, tun_name); google::protobuf::ShutdownProtobufLibrary(); return 0; } diff --git a/src/common/tun.cpp b/src/common/tun.cpp new file mode 100644 index 0000000..d99d481 --- /dev/null +++ b/src/common/tun.cpp @@ -0,0 +1,117 @@ +#include "common/tun.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +int tun_open(const std::string& name, uint32_t ipv4, uint8_t prefix_len) { + // 1. Open the TUN device file --- + int fd = open("/dev/net/tun", O_RDWR | O_NONBLOCK); + if (fd < 0) { + perror("[TUN] open /dev/net/tun"); + return -1; + } + + // 2. Configure as TUN (no packet info header) --- + struct ifreq ifr{}; + ifr.ifr_flags = IFF_TUN | IFF_NO_PI; + strncpy(ifr.ifr_name, name.c_str(), IFNAMSIZ - 1); + + if (ioctl(fd, TUNSETIFF, &ifr) < 0) { + perror("[TUN] ioctl TUNSETIFF"); + close(fd); + return -1; + } + + // 3. Assign IPv4 address via a temporary socket --- + int sock = socket(AF_INET, SOCK_DGRAM, 0); + if (sock < 0) { + perror("[TUN] socket"); + close(fd); + return -1; + } + + // Set IP address + struct ifreq req_addr{}; + strncpy(req_addr.ifr_name, name.c_str(), IFNAMSIZ - 1); + auto* addr = reinterpret_cast(&req_addr.ifr_addr); + addr->sin_family = AF_INET; + addr->sin_addr.s_addr = ipv4; + + if (ioctl(sock, SIOCSIFADDR, &req_addr) < 0) { + perror("[TUN] ioctl SIOCSIFADDR"); + close(sock); + close(fd); + return -1; + } + + // Set netmask + struct ifreq req_mask{}; + strncpy(req_mask.ifr_name, name.c_str(), IFNAMSIZ - 1); + auto* mask = reinterpret_cast(&req_mask.ifr_netmask); + mask->sin_family = AF_INET; + + uint32_t netmask = prefix_len == 0 ? 0 : htonl(~((1u << (32 - prefix_len)) - 1)); + mask->sin_addr.s_addr = netmask; + + if (ioctl(sock, SIOCSIFNETMASK, &req_mask) < 0) { + perror("[TUN] ioctl SIOCSIFNETMASK"); + close(sock); + close(fd); + return -1; + } + + // Bring interface up + struct ifreq req_flags{}; + strncpy(req_flags.ifr_name, name.c_str(), IFNAMSIZ - 1); + + if (ioctl(sock, SIOCGIFFLAGS, &req_flags) < 0) { + perror("[TUN] ioctl SIOCGIFFLAGS"); + close(sock); + close(fd); + return -1; + } + + req_flags.ifr_flags |= IFF_UP | IFF_RUNNING; + + if (ioctl(sock, SIOCSIFFLAGS, &req_flags) < 0) { + perror("[TUN] ioctl SIOCSIFFLAGS"); + close(sock); + close(fd); + return -1; + } + + close(sock); + + struct in_addr addr_print; + addr_print.s_addr = ipv4; + std::cout << "[TUN] Interface " << name + << " up with IP " << inet_ntoa(addr_print) + << "/" << (int)prefix_len << "\n"; + + return fd; +} + +void tun_close(int fd, const std::string& name) { + // Bring interface down + int sock = socket(AF_INET, SOCK_DGRAM, 0); + if (sock >= 0) { + struct ifreq ifr{}; + strncpy(ifr.ifr_name, name.c_str(), IFNAMSIZ - 1); + if (ioctl(sock, SIOCGIFFLAGS, &ifr) == 0) { + ifr.ifr_flags &= ~IFF_UP; + ioctl(sock, SIOCSIFFLAGS, &ifr); + } + close(sock); + } + close(fd); +} diff --git a/src/conductor/setup.cpp b/src/conductor/setup.cpp index cb9c07b..f150c4d 100644 --- a/src/conductor/setup.cpp +++ b/src/conductor/setup.cpp @@ -247,6 +247,16 @@ struct ConductorDispatch : public Dispatch { info->fd = data.fd; info->connected_at_ms = now_ms(); + // Reject if an agent with this name is already connected + for (const auto* existing : g_state.agents) { + if (existing->name == pkt.name()) { + std::cerr << "[Conductor] Rejected duplicate agent: " + << pkt.name() << "\n"; + queue->close(data.fd); + return; + } + } + ConnInfo* sw = g_state.pick_switch(pkt.network()); if (!sw) { std::cerr << "[Conductor] No available switch for agent " diff --git a/src/protocol/dispatch.cpp b/src/protocol/dispatch.cpp index e2af272..85025a1 100644 --- a/src/protocol/dispatch.cpp +++ b/src/protocol/dispatch.cpp @@ -1,4 +1,5 @@ #include "vnet/protocol/dispatch.hpp" +#include "vnet/protocol/types.hpp" using namespace vnet::protocol; using namespace vnet::netqueue; @@ -88,6 +89,27 @@ void Dispatch::onSocketReady(socket_data data) { } break; } + case PREPARE_ROUTE_FOR_TARGET: { + mip::PacketPrepareRouteForTarget packet; + if (packet.ParseFromArray(data.packet_buffer, data.payload_size)) { + onPrepareRouteForTarget(data, packet); + } + break; + } + case NEXT_FOR_TARGET: { + mip::PacketNextForTarget packet; + if (packet.ParseFromArray(data.packet_buffer, data.payload_size)) { + onNextForTarget(data, packet); + } + break; + } + case IPV4_RAW: { + mip::PacketIPv4Raw packet; + if (packet.ParseFromArray(data.packet_buffer, data.payload_size)) { + onIPv4Raw(data, packet); + } + break; + } default: break; } @@ -110,3 +132,7 @@ void Dispatch::onAgentConnectionToken(socket_data, mip::PacketAgentConnectionTok void Dispatch::onConnectionAccepted (socket_data, mip::PacketConnectionAccepted&) {} void Dispatch::onAgentMRP (socket_data, mip::PacketAgentMRP&) {} + +void Dispatch::onPrepareRouteForTarget (socket_data, mip::PacketPrepareRouteForTarget&) {} +void Dispatch::onNextForTarget (socket_data, mip::PacketNextForTarget&) {} +void Dispatch::onIPv4Raw (socket_data, mip::PacketIPv4Raw&) {} diff --git a/src/switch/setup.cpp b/src/switch/setup.cpp index 73f8809..2a45d93 100644 --- a/src/switch/setup.cpp +++ b/src/switch/setup.cpp @@ -20,9 +20,12 @@ #include "vnet/netqueue/handler.hpp" #include "vnet/protocol/dispatch.hpp" #include "vnet/netqueue/netqueue.hpp" +#include "vnet/blackbox/blackbox.hpp" +#include "vnet/blackbox/config.hpp" using namespace vnet::protocol; using namespace vnet::netqueue; +using namespace vnet::blackbox; using clk = std::chrono::steady_clock; static const int LISTEN_BACKLOG = 64; @@ -77,6 +80,7 @@ struct SwitchState { }; static SwitchState g_state; +static BlackBox* g_blackbox = nullptr; // --------------------------------------------------------------------------- // Dispatch @@ -105,34 +109,54 @@ struct SwitchDispatch : public Dispatch { } /* - * An agent that has connected to us sends its token. - * Validate and either accept or drop. - */ + * An agent that has connected to us sends its token. + * Validate and either accept or drop. + */ void onAuthConnectToSwitch(socket_data data, - mip::PacketAuthConnectToSwitch& pkt) override { - auto* info = static_cast(data.ptr_data); + mip::PacketAuthConnectToSwitch& pkt) override { + auto* info = static_cast(data.ptr_data); uint64_t token = pkt.connection_token(); auto it = g_state.pending_tokens.find(token); if (it == g_state.pending_tokens.end()) { std::cerr << "[Switch] Rejected agent: invalid token " - << token << "\n"; - queue->close(data.fd); - return; // NetQueue will see no further reads → eventually close + << token << "\n"; + data.net_element->state = SCK_ERROR; + return; } - info->role = ConnRole::AGENT_AUTHENTICATED; - info->name = it->second.agent_name; - + std::string agent_name = it->second.agent_name; g_state.pending_tokens.erase(it); + + // Register agent in BlackBox — looks up virtual IP from config + if (!g_blackbox->on_agent_authenticated(agent_name, data.fd)) { + std::cerr << "[Switch] Agent " << agent_name + << " not found in config, rejecting\n"; + data.net_element->state = SCK_ERROR; + return; + } + + // Get the virtual IP the BlackBox assigned + AgentEntry* entry = g_blackbox->agents().find_by_name(agent_name); + if (!entry) { + std::cerr << "[Switch] Failed to get agent entry for " + << agent_name << "\n"; + data.net_element->state = SCK_ERROR; + return; + } + + info->role = ConnRole::AGENT_AUTHENTICATED; + info->name = agent_name; g_state.agents.push_back(info); - // Send acceptance + // Send acceptance with virtual IP so agent can set up its TUN mip::PacketConnectionAccepted ack; + ack.set_virtual_ipv4(entry->virtual_ipv4); queue->send(data.fd, PacketType::CONNECTION_ACCEPTED, ack); std::cout << "[Switch] Agent " << info->name - << " authenticated (fd=" << data.fd << ")\n"; + << " authenticated (fd=" << data.fd + << ", ip=" << ipv4_to_string(entry->virtual_ipv4) << ")\n"; } void onHeartbeat(socket_data) override {} @@ -143,10 +167,12 @@ struct SwitchDispatch : public Dispatch { if (info->role != ConnRole::AGENT_PENDING) { std::cout << "[Switch] Connection closed: " << info->name - << " (fd=" << data.fd << ")\n"; + << " (fd=" << data.fd << ")\n"; } if (info->role == ConnRole::AGENT_AUTHENTICATED) { + // Notify BlackBox so it cleans up the agent registry + g_blackbox->on_agent_disconnected(data.fd); g_state.agents.erase( std::remove(g_state.agents.begin(), g_state.agents.end(), info), g_state.agents.end()); @@ -216,10 +242,27 @@ int main(int argc, char** argv) { // --- Read conductor address from environment --- const char* cdt_ip_env = std::getenv("CONDUCTOR_IP"); const char* cdt_port_env = std::getenv("CONDUCTOR_PORT"); + const char* config_path = std::getenv("VNET_CONFIG_PATH"); if (!cdt_ip_env || !cdt_port_env) { std::cerr << "[Switch] CONDUCTOR_IP and CONDUCTOR_PORT must be set\n"; return 1; } + if (!config_path) { + std::cerr << "[Switch] VNET_CONFIG_PATH must be set\n"; + return 1; + } + + // --- Load config and initialize BlackBox --- + vnet::blackbox::Config config; + if (!config.load(config_path)) { + std::cerr << "[Switch] Failed to load config: " << config_path << "\n"; + return 1; + } + + BlackBox blackbox(config, -1); // -1 = no internet TUN yet + g_blackbox = &blackbox; + + std::cout << "[Switch] Loaded config from " << config_path << "\n"; MachineConfig conductor_cfg { cdt_ip_env,