diff --git a/src/platform/backends/qemu/linux/dnsmasq_process_spec.cpp b/src/platform/backends/qemu/linux/dnsmasq_process_spec.cpp index 04fc0f4fbf..2f95cd0cdd 100644 --- a/src/platform/backends/qemu/linux/dnsmasq_process_spec.cpp +++ b/src/platform/backends/qemu/linux/dnsmasq_process_spec.cpp @@ -24,11 +24,33 @@ namespace mp = multipass; namespace mpu = multipass::utils; +namespace +{ +[[nodiscard]] QStringList make_dnsmasq_subnet_args(const mp::SubnetList& subnets) +{ + QStringList out{}; + for (const auto& [bridge_name, subnet] : subnets) + { + const auto bridge_addr = mp::IPAddress{fmt::format("{}.1", subnet)}; + const auto start_ip = mp::IPAddress{fmt::format("{}.2", subnet)}; + const auto end_ip = mp::IPAddress{fmt::format("{}.254", subnet)}; + + out << QString("--interface=%1").arg(bridge_name) + << QString("--listen-address=%1").arg(QString::fromStdString(bridge_addr.as_string())) + << "--dhcp-range" + << QString("%1,%2,infinite") + .arg(QString::fromStdString(start_ip.as_string())) + .arg(QString::fromStdString(end_ip.as_string())); + } + + return out; +} +} // namespace + mp::DNSMasqProcessSpec::DNSMasqProcessSpec(const mp::Path& data_dir, - const QString& bridge_name, - const std::string& subnet, + const SubnetList& subnets, const QString& conf_file_path) - : data_dir(data_dir), bridge_name(bridge_name), subnet{subnet}, conf_file_path{conf_file_path} + : data_dir(data_dir), subnets(subnets), conf_file_path{conf_file_path} { } @@ -39,26 +61,20 @@ QString mp::DNSMasqProcessSpec::program() const QStringList mp::DNSMasqProcessSpec::arguments() const { - const auto bridge_addr = mp::IPAddress{fmt::format("{}.1", subnet)}; - const auto start_ip = mp::IPAddress{fmt::format("{}.2", subnet)}; - const auto end_ip = mp::IPAddress{fmt::format("{}.254", subnet)}; - - return QStringList() - << "--keep-in-foreground" - << "--strict-order" - << "--bind-interfaces" << QString("--pid-file") << "--domain=multipass" - << "--local=/multipass/" - << "--except-interface=lo" << QString("--interface=%1").arg(bridge_name) - << QString("--listen-address=%1").arg(QString::fromStdString(bridge_addr.as_string())) - << "--dhcp-no-override" - << "--dhcp-ignore-clid" - << "--dhcp-authoritative" << QString("--dhcp-leasefile=%1/dnsmasq.leases").arg(data_dir) - << QString("--dhcp-hostsfile=%1/dnsmasq.hosts").arg(data_dir) << "--dhcp-range" - << QString("%1,%2,infinite") - .arg(QString::fromStdString(start_ip.as_string())) - .arg(QString::fromStdString(end_ip.as_string())) - // This is to prevent it trying to read /etc/dnsmasq.conf - << QString("--conf-file=%1").arg(conf_file_path); + auto out = QStringList() << "--keep-in-foreground" + << "--strict-order" + << "--bind-interfaces" << QString("--pid-file") << "--domain=multipass" + << "--local=/multipass/" + << "--except-interface=lo" + << "--dhcp-no-override" + << "--dhcp-ignore-clid" + << "--dhcp-authoritative" + << QString("--dhcp-leasefile=%1/dnsmasq.leases").arg(data_dir) + << QString("--dhcp-hostsfile=%1/dnsmasq.hosts").arg(data_dir) + // This is to prevent it trying to read /etc/dnsmasq.conf + << QString("--conf-file=%1").arg(conf_file_path); + + return out << make_dnsmasq_subnet_args(subnets); } mp::logging::Level mp::DNSMasqProcessSpec::error_log_level() const diff --git a/src/platform/backends/qemu/linux/dnsmasq_process_spec.h b/src/platform/backends/qemu/linux/dnsmasq_process_spec.h index 36d6be9804..e37d336622 100644 --- a/src/platform/backends/qemu/linux/dnsmasq_process_spec.h +++ b/src/platform/backends/qemu/linux/dnsmasq_process_spec.h @@ -17,7 +17,8 @@ #pragma once -#include +#include "dnsmasq_server.h" + #include #include @@ -30,8 +31,7 @@ class DNSMasqProcessSpec : public ProcessSpec { public: explicit DNSMasqProcessSpec(const Path& data_dir, - const QString& bridge_name, - const std::string& subnet, + const SubnetList& subnets, const QString& conf_file_path); QString program() const override; @@ -42,8 +42,7 @@ class DNSMasqProcessSpec : public ProcessSpec private: const Path data_dir; - const QString bridge_name; - const std::string subnet; + const SubnetList subnets; const QString conf_file_path; }; diff --git a/src/platform/backends/qemu/linux/dnsmasq_server.cpp b/src/platform/backends/qemu/linux/dnsmasq_server.cpp index 62f7acbb27..f1a69f244c 100644 --- a/src/platform/backends/qemu/linux/dnsmasq_server.cpp +++ b/src/platform/backends/qemu/linux/dnsmasq_server.cpp @@ -36,23 +36,16 @@ namespace constexpr auto immediate_wait = 100; // period to wait for immediate dnsmasq failures, in ms auto make_dnsmasq_process(const mp::Path& data_dir, - const QString& bridge_name, - const std::string& subnet, + const mp::SubnetList& subnets, const QString& conf_file_path) { - auto process_spec = - std::make_unique(data_dir, bridge_name, subnet, conf_file_path); + auto process_spec = std::make_unique(data_dir, subnets, conf_file_path); return MP_PROCFACTORY.create_process(std::move(process_spec)); } } // namespace -mp::DNSMasqServer::DNSMasqServer(const Path& data_dir, - const QString& bridge_name, - const std::string& subnet) - : data_dir{data_dir}, - bridge_name{bridge_name}, - subnet{subnet}, - conf_file{QDir(data_dir).absoluteFilePath("dnsmasq-XXXXXX.conf")} +mp::DNSMasqServer::DNSMasqServer(const Path& data_dir, const SubnetList& subnets) + : data_dir{data_dir}, conf_file{QDir(data_dir).absoluteFilePath("dnsmasq-XXXXXX.conf")} { conf_file.open(); conf_file.close(); @@ -63,7 +56,7 @@ mp::DNSMasqServer::DNSMasqServer(const Path& data_dir, dnsmasq_hosts.open(QIODevice::WriteOnly); } - dnsmasq_cmd = make_dnsmasq_process(data_dir, bridge_name, subnet, conf_file.fileName()); + dnsmasq_cmd = make_dnsmasq_process(data_dir, subnets, conf_file.fileName()); start_dnsmasq(); } @@ -106,7 +99,7 @@ std::optional mp::DNSMasqServer::get_ip_for(const std::string& hw return std::nullopt; } -void mp::DNSMasqServer::release_mac(const std::string& hw_addr) +void mp::DNSMasqServer::release_mac(const std::string& hw_addr, const QString& bridge_name) { auto ip = get_ip_for(hw_addr); if (!ip) @@ -207,8 +200,7 @@ void mp::DNSMasqServer::start_dnsmasq() mp::DNSMasqServer::UPtr mp::DNSMasqServerFactory::make_dnsmasq_server( const mp::Path& network_dir, - const QString& bridge_name, - const std::string& subnet) const + const SubnetList& subnets) const { - return std::make_unique(network_dir, bridge_name, subnet); + return std::make_unique(network_dir, subnets); } diff --git a/src/platform/backends/qemu/linux/dnsmasq_server.h b/src/platform/backends/qemu/linux/dnsmasq_server.h index 32092f85e3..b3b58e20eb 100644 --- a/src/platform/backends/qemu/linux/dnsmasq_server.h +++ b/src/platform/backends/qemu/linux/dnsmasq_server.h @@ -32,16 +32,18 @@ namespace multipass { class Process; +using SubnetList = std::vector>; + class DNSMasqServer : private DisabledCopyMove { public: using UPtr = std::unique_ptr; - DNSMasqServer(const Path& data_dir, const QString& bridge_name, const std::string& subnet); + DNSMasqServer(const Path& data_dir, const SubnetList& subnets); virtual ~DNSMasqServer(); // inherited by mock for testing virtual std::optional get_ip_for(const std::string& hw_addr); - virtual void release_mac(const std::string& hw_addr); + virtual void release_mac(const std::string& hw_addr, const QString& bridge_name); virtual void check_dnsmasq_running(); protected: @@ -51,8 +53,6 @@ class DNSMasqServer : private DisabledCopyMove void start_dnsmasq(); const QString data_dir; - const QString bridge_name; - const std::string subnet; std::unique_ptr dnsmasq_cmd; QMetaObject::Connection finish_connection; QTemporaryFile conf_file; @@ -67,7 +67,6 @@ class DNSMasqServerFactory : public Singleton : Singleton::Singleton{pass} {}; virtual DNSMasqServer::UPtr make_dnsmasq_server(const Path& network_dir, - const QString& bridge_name, - const std::string& subnet) const; + const SubnetList& subnets) const; }; } // namespace multipass diff --git a/src/platform/backends/qemu/linux/qemu_platform_detail.h b/src/platform/backends/qemu/linux/qemu_platform_detail.h index 07b5a48b14..a4d571d6ea 100644 --- a/src/platform/backends/qemu/linux/qemu_platform_detail.h +++ b/src/platform/backends/qemu/linux/qemu_platform_detail.h @@ -46,11 +46,27 @@ class QemuPlatformDetail : public QemuPlatform void set_authorization(std::vector& networks) override; private: - const QString bridge_name; + // explicitly naming DisabledCopyMove since the private one derived from QemuPlatform takes + // precedence in lookup + struct Subnet : private multipass::DisabledCopyMove + { + const QString bridge_name; + const std::string subnet; + FirewallConfig::UPtr firewall_config; + + Subnet(const Path& network_dir, const std::string& name); + ~Subnet(); + }; + using Subnets = std::unordered_map; + + [[nodiscard]] static Subnets get_subnets(const Path& network_dir); + + [[nodiscard]] static SubnetList get_subnets_list(const Subnets&); + const Path network_dir; - const std::string subnet; + const Subnets subnets; DNSMasqServer::UPtr dnsmasq_server; - FirewallConfig::UPtr firewall_config; - std::unordered_map> name_to_net_device_map; + std::unordered_map> + name_to_net_device_map; }; } // namespace multipass diff --git a/src/platform/backends/qemu/linux/qemu_platform_detail_linux.cpp b/src/platform/backends/qemu/linux/qemu_platform_detail_linux.cpp index 27e0178462..a0bebd5a4c 100644 --- a/src/platform/backends/qemu/linux/qemu_platform_detail_linux.cpp +++ b/src/platform/backends/qemu/linux/qemu_platform_detail_linux.cpp @@ -15,6 +15,7 @@ * */ +#include "multipass/constants.h" #include "qemu_platform_detail.h" #include @@ -35,7 +36,7 @@ namespace mpu = multipass::utils; namespace { constexpr auto category = "qemu platform"; -const QString multipass_bridge_name{"mpqemubr0"}; +const QString multipass_bridge_name{"mpqemubr%1"}; // An interface name can only be 15 characters, so this generates a hash of the // VM instance name with a "tap-" prefix and then truncates it. @@ -103,14 +104,10 @@ void set_ip_forward() } } -mp::DNSMasqServer::UPtr init_nat_network(const mp::Path& network_dir, - const QString& bridge_name, - const std::string& subnet) +mp::DNSMasqServer::UPtr init_nat_network(const mp::Path& network_dir, const mp::SubnetList& subnets) { - create_virtual_switch(subnet, bridge_name); set_ip_forward(); - - return MP_DNSMASQ_SERVER_FACTORY.make_dnsmasq_server(network_dir, bridge_name, subnet); + return MP_DNSMASQ_SERVER_FACTORY.make_dnsmasq_server(network_dir, subnets); } void delete_virtual_switch(const QString& bridge_name) @@ -122,12 +119,52 @@ void delete_virtual_switch(const QString& bridge_name) } } // namespace -mp::QemuPlatformDetail::QemuPlatformDetail(const mp::Path& data_dir) - : bridge_name{multipass_bridge_name}, - network_dir{MP_UTILS.make_dir(QDir(data_dir), "network")}, +mp::QemuPlatformDetail::Subnet::Subnet(const Path& network_dir, const std::string& name) + : bridge_name{multipass_bridge_name.arg(name.c_str())}, subnet{MP_BACKEND.get_subnet(network_dir, bridge_name)}, - dnsmasq_server{init_nat_network(network_dir, bridge_name, subnet)}, firewall_config{MP_FIREWALL_CONFIG_FACTORY.make_firewall_config(bridge_name, subnet)} +{ + create_virtual_switch(subnet, bridge_name); +} + +mp::QemuPlatformDetail::Subnet::~Subnet() +{ + delete_virtual_switch(bridge_name); +} + +[[nodiscard]] mp::QemuPlatformDetail::Subnets mp::QemuPlatformDetail::get_subnets( + const Path& network_dir) +{ + Subnets subnets{}; + subnets.reserve(default_zone_names.size()); + + for (const auto& zone : default_zone_names) + { + subnets.emplace(std::piecewise_construct, + std::forward_as_tuple(zone), + std::forward_as_tuple(network_dir, zone)); + } + + return subnets; +} + +[[nodiscard]] mp::SubnetList mp::QemuPlatformDetail::get_subnets_list(const Subnets& subnets) +{ + SubnetList out{}; + out.reserve(subnets.size()); + + for (const auto& [_, subnet] : subnets) + { + out.emplace_back(subnet.bridge_name, subnet.subnet); + } + + return out; +} + +mp::QemuPlatformDetail::QemuPlatformDetail(const mp::Path& data_dir) + : network_dir{MP_UTILS.make_dir(QDir(data_dir), "network")}, + subnets{get_subnets(network_dir)}, + dnsmasq_server{init_nat_network(network_dir, get_subnets_list(subnets))} { } @@ -135,11 +172,9 @@ mp::QemuPlatformDetail::~QemuPlatformDetail() { for (const auto& it : name_to_net_device_map) { - const auto& [tap_device_name, hw_addr] = it.second; + const auto& [tap_device_name, hw_addr, _] = it.second; remove_tap_device(tap_device_name); } - - delete_virtual_switch(bridge_name); } std::optional mp::QemuPlatformDetail::get_ip_for(const std::string& hw_addr) @@ -152,8 +187,8 @@ void mp::QemuPlatformDetail::remove_resources_for(const std::string& name) auto it = name_to_net_device_map.find(name); if (it != name_to_net_device_map.end()) { - const auto& [tap_device_name, hw_addr] = it->second; - dnsmasq_server->release_mac(hw_addr); + const auto& [tap_device_name, hw_addr, bridge_name] = it->second; + dnsmasq_server->release_mac(hw_addr, bridge_name); remove_tap_device(tap_device_name); name_to_net_device_map.erase(name); @@ -166,17 +201,22 @@ void mp::QemuPlatformDetail::platform_health_check() MP_BACKEND.check_if_kvm_is_in_use(); dnsmasq_server->check_dnsmasq_running(); - firewall_config->verify_firewall_rules(); + for (const auto& [_, subnet] : subnets) + { + subnet.firewall_config->verify_firewall_rules(); + } } QStringList mp::QemuPlatformDetail::vm_platform_args(const VirtualMachineDescription& vm_desc) { // Configure and generate the args for the default network interface auto tap_device_name = generate_tap_device_name(vm_desc.vm_name); + const QString& bridge_name = subnets.at(vm_desc.zone).bridge_name; create_tap_device(tap_device_name, bridge_name); - name_to_net_device_map.emplace(vm_desc.vm_name, - std::make_pair(tap_device_name, vm_desc.default_mac_address)); + name_to_net_device_map.emplace( + vm_desc.vm_name, + std::make_tuple(tap_device_name, vm_desc.default_mac_address, bridge_name)); QStringList opts; diff --git a/src/platform/backends/shared/linux/backend_utils.cpp b/src/platform/backends/shared/linux/backend_utils.cpp index 81b706df67..cb291e0c36 100644 --- a/src/platform/backends/shared/linux/backend_utils.cpp +++ b/src/platform/backends/shared/linux/backend_utils.cpp @@ -55,7 +55,11 @@ Q_DECLARE_METATYPE(VariantMapMap) // for DBus namespace { -std::default_random_engine gen; +std::default_random_engine gen = [] { + // seed the rng with the time at initialization + auto seed = std::chrono::system_clock::now().time_since_epoch().count(); + return std::default_random_engine(seed); +}(); std::uniform_int_distribution dist{0, 255}; const auto nm_bus_name = QStringLiteral("org.freedesktop.NetworkManager"); const auto nm_root_obj = QStringLiteral("/org/freedesktop/NetworkManager"); @@ -75,7 +79,7 @@ bool subnet_used_locally(const std::string& subnet) bool can_reach_gateway(const std::string& ip) { - return MP_UTILS.run_cmd_for_status("ping", {"-n", "-q", ip.c_str(), "-c", "-1", "-W", "1"}); + return MP_UTILS.run_cmd_for_status("ping", {"-n", "-q", ip.c_str(), "-c", "1", "-W", "1"}); } auto virtual_switch_subnet(const QString& bridge_name) @@ -197,7 +201,7 @@ auto make_bridge_rollback_guard(std::string_view log_category, std::string mp::backend::generate_random_subnet() { - gen.seed(std::chrono::system_clock::now().time_since_epoch().count()); + // TODO don't rely on pure randomness for (auto i = 0; i < 100; ++i) { auto subnet = fmt::format("10.{}.{}", dist(gen), dist(gen)); @@ -280,7 +284,7 @@ std::string mp::Backend::get_subnet(const mp::Path& network_dir, const QString& if (!subnet.empty()) return subnet; - QFile subnet_file{network_dir + "/multipass_subnet"}; + QFile subnet_file{network_dir + "/multipass_subnet_" + bridge_name}; MP_FILEOPS.open(subnet_file, QIODevice::ReadWrite | QIODevice::Text); if (MP_FILEOPS.size(subnet_file) > 0) return MP_FILEOPS.read_all(subnet_file).trimmed().toStdString(); diff --git a/tests/qemu/linux/mock_dnsmasq_server.h b/tests/qemu/linux/mock_dnsmasq_server.h index 929fe2e861..337dd90ea5 100644 --- a/tests/qemu/linux/mock_dnsmasq_server.h +++ b/tests/qemu/linux/mock_dnsmasq_server.h @@ -31,7 +31,7 @@ struct MockDNSMasqServer : public DNSMasqServer using DNSMasqServer::DNSMasqServer; // ctor MOCK_METHOD(std::optional, get_ip_for, (const std::string&), (override)); - MOCK_METHOD(void, release_mac, (const std::string&), (override)); + MOCK_METHOD(void, release_mac, (const std::string&, const QString&), (override)); MOCK_METHOD(void, check_dnsmasq_running, (), (override)); }; @@ -41,7 +41,7 @@ struct MockDNSMasqServerFactory : public DNSMasqServerFactory MOCK_METHOD(DNSMasqServer::UPtr, make_dnsmasq_server, - (const Path&, const QString&, const std::string&), + (const Path&, (const SubnetList&)), (const, override)); MP_MOCK_SINGLETON_BOILERPLATE(MockDNSMasqServerFactory, DNSMasqServerFactory); diff --git a/tests/qemu/linux/test_dnsmasq_process_spec.cpp b/tests/qemu/linux/test_dnsmasq_process_spec.cpp index 0db9435303..8d6d18aa0e 100644 --- a/tests/qemu/linux/test_dnsmasq_process_spec.cpp +++ b/tests/qemu/linux/test_dnsmasq_process_spec.cpp @@ -32,8 +32,7 @@ using namespace testing; struct TestDnsmasqProcessSpec : public Test { const QString data_dir{"/data"}; - const QString bridge_name{"bridgey"}; - const std::string subnet{"1.2.3"}; + const mp::SubnetList subnets{{"bridgey", "1.2.3"}}; const QString conf_file_path{"/path/to/file.conf"}; }; @@ -43,7 +42,7 @@ TEST_F(TestDnsmasqProcessSpec, defaultArgumentsCorrect) mpt::SetEnvScope e1("SNAP", "/something"); mpt::SetEnvScope e2("SNAP_NAME", snap_name); - mp::DNSMasqProcessSpec spec(data_dir, bridge_name, subnet, conf_file_path); + mp::DNSMasqProcessSpec spec(data_dir, subnets, conf_file_path); EXPECT_EQ(spec.arguments(), QStringList({"--keep-in-foreground", "--strict-order", @@ -52,28 +51,28 @@ TEST_F(TestDnsmasqProcessSpec, defaultArgumentsCorrect) "--domain=multipass", "--local=/multipass/", "--except-interface=lo", - "--interface=bridgey", - "--listen-address=1.2.3.1", "--dhcp-no-override", "--dhcp-ignore-clid", "--dhcp-authoritative", "--dhcp-leasefile=/data/dnsmasq.leases", "--dhcp-hostsfile=/data/dnsmasq.hosts", + "--conf-file=/path/to/file.conf", + "--interface=bridgey", + "--listen-address=1.2.3.1", "--dhcp-range", - "1.2.3.2,1.2.3.254,infinite", - "--conf-file=/path/to/file.conf"})); + "1.2.3.2,1.2.3.254,infinite"})); } TEST_F(TestDnsmasqProcessSpec, apparmorProfileHasCorrectName) { - mp::DNSMasqProcessSpec spec(data_dir, bridge_name, subnet, conf_file_path); + mp::DNSMasqProcessSpec spec(data_dir, subnets, conf_file_path); EXPECT_TRUE(spec.apparmor_profile().contains("profile multipass.dnsmasq")); } TEST_F(TestDnsmasqProcessSpec, apparmorProfilePermitsDataDirs) { - mp::DNSMasqProcessSpec spec(data_dir, bridge_name, subnet, conf_file_path); + mp::DNSMasqProcessSpec spec(data_dir, subnets, conf_file_path); EXPECT_TRUE(spec.apparmor_profile().contains("/data/dnsmasq.leases rw,")); EXPECT_TRUE(spec.apparmor_profile().contains("/data/dnsmasq.hosts r,")); @@ -82,7 +81,7 @@ TEST_F(TestDnsmasqProcessSpec, apparmorProfilePermitsDataDirs) TEST_F(TestDnsmasqProcessSpec, apparmorProfileIdentifier) { - mp::DNSMasqProcessSpec spec(data_dir, bridge_name, subnet, conf_file_path); + mp::DNSMasqProcessSpec spec(data_dir, subnets, conf_file_path); EXPECT_EQ(spec.identifier(), ""); } @@ -94,7 +93,7 @@ TEST_F(TestDnsmasqProcessSpec, apparmorProfileRunningAsSnapCorrect) mpt::SetEnvScope e1("SNAP", snap_dir.path().toUtf8()); mpt::SetEnvScope e2("SNAP_NAME", snap_name); - mp::DNSMasqProcessSpec spec(data_dir, bridge_name, subnet, conf_file_path); + mp::DNSMasqProcessSpec spec(data_dir, subnets, conf_file_path); EXPECT_TRUE( spec.apparmor_profile().contains("signal (receive) peer=snap.multipass.multipassd")); @@ -112,7 +111,7 @@ TEST_F(TestDnsmasqProcessSpec, apparmorProfileRunningAsSymlinkedSnapCorrect) mpt::SetEnvScope e1("SNAP", link_dir.path().toUtf8()); mpt::SetEnvScope e2("SNAP_NAME", snap_name); - mp::DNSMasqProcessSpec spec(data_dir, bridge_name, subnet, conf_file_path); + mp::DNSMasqProcessSpec spec(data_dir, subnets, conf_file_path); EXPECT_TRUE( spec.apparmor_profile().contains(QString("%1/usr/sbin/dnsmasq ixr,").arg(snap_dir.path()))); @@ -124,7 +123,7 @@ TEST_F(TestDnsmasqProcessSpec, apparmorProfileNotRunningAsSnapCorrect) mpt::UnsetEnvScope e("SNAP"); mpt::SetEnvScope e2("SNAP_NAME", snap_name); - mp::DNSMasqProcessSpec spec(data_dir, bridge_name, subnet, conf_file_path); + mp::DNSMasqProcessSpec spec(data_dir, subnets, conf_file_path); EXPECT_TRUE(spec.apparmor_profile().contains("signal (receive) peer=unconfined")); EXPECT_TRUE(spec.apparmor_profile().contains(" /usr/sbin/dnsmasq ixr,")); // space wanted diff --git a/tests/qemu/linux/test_dnsmasq_server.cpp b/tests/qemu/linux/test_dnsmasq_server.cpp index 43e326bbd4..c2b72190b0 100644 --- a/tests/qemu/linux/test_dnsmasq_server.cpp +++ b/tests/qemu/linux/test_dnsmasq_server.cpp @@ -87,8 +87,9 @@ struct DNSMasqServer : public mpt::TestWithMockedBinPath mpt::ResetProcessFactory scope; // will otherwise pollute other tests mpt::TempDir data_dir; std::shared_ptr logger = std::make_shared(); - const QString bridge_name{"dummy-bridge"}; - const std::string subnet{"192.168.64"}; + + const QString dummy_bridge{"dummy-bridge"}; + const std::string default_subnet{"192.168.64"}; const std::string error_subnet{ "0.0.0"}; // This forces the mock dnsmasq process to exit with error const std::string hw_addr{"00:01:02:03:04:05"}; @@ -97,15 +98,29 @@ struct DNSMasqServer : public mpt::TestWithMockedBinPath "0 "s + hw_addr + " "s + expected_ip + " dummy_name 00:01:02:03:04:05:06:07:08:09:0a:0b:0c:0d:0e:0f:10:11:12"; - mp::DNSMasqServer make_default_dnsmasq_server() + [[nodiscard]] static mp::SubnetList make_subnets(const QString& bridge, + const std::string& subnet) + { + return {{bridge, subnet}}; + } + + mp::DNSMasqServer make_default_dnsmasq_server() const { - return mp::DNSMasqServer{data_dir.path(), bridge_name, subnet}; + return mp::DNSMasqServer{data_dir.path(), make_subnets(dummy_bridge, default_subnet)}; } }; TEST_F(DNSMasqServer, startsDnsmasqProcess) { - EXPECT_NO_THROW(mp::DNSMasqServer dns(data_dir.path(), bridge_name, subnet)); + EXPECT_NO_THROW( + mp::DNSMasqServer dns(data_dir.path(), make_subnets(dummy_bridge, default_subnet))); +} + +TEST_F(DNSMasqServer, factory_creates_dnsmasq_process) +{ + EXPECT_NO_THROW( + MP_DNSMASQ_SERVER_FACTORY.make_dnsmasq_server(data_dir.path(), + make_subnets(dummy_bridge, default_subnet))); } TEST_F(DNSMasqServer, findsIp) @@ -133,10 +148,13 @@ TEST_F(DNSMasqServer, releaseMacReleasesIp) { const QString dhcp_release_called{QDir{data_dir.path()}.filePath("dhcp_release_called")}; - mp::DNSMasqServer dns{data_dir.path(), dhcp_release_called, subnet}; + auto subnets = make_subnets(dhcp_release_called, default_subnet); + ASSERT_EQ(subnets.size(), 1); + + mp::DNSMasqServer dns{data_dir.path(), subnets}; make_lease_entry(); - dns.release_mac(hw_addr); + dns.release_mac(hw_addr, subnets.front().first); EXPECT_TRUE(QFile::exists(dhcp_release_called)); } @@ -145,8 +163,11 @@ TEST_F(DNSMasqServer, releaseMacLogsFailureOnMissingIp) { const QString dhcp_release_called{QDir{data_dir.path()}.filePath("dhcp_release_called")}; - mp::DNSMasqServer dns{data_dir.path(), dhcp_release_called, subnet}; - dns.release_mac(hw_addr); + auto subnets = make_subnets(dhcp_release_called, default_subnet); + ASSERT_EQ(subnets.size(), 1); + + mp::DNSMasqServer dns{data_dir.path(), subnets}; + dns.release_mac(hw_addr, subnets.front().first); EXPECT_FALSE(QFile::exists(dhcp_release_called)); EXPECT_TRUE(logger->logged_lines.size() > 0); @@ -156,10 +177,13 @@ TEST_F(DNSMasqServer, releaseMacLogsFailures) { const QString dhcp_release_called{QDir{data_dir.path()}.filePath("dhcp_release_called.fail")}; - mp::DNSMasqServer dns{data_dir.path(), dhcp_release_called, subnet}; + auto subnets = make_subnets(dhcp_release_called, default_subnet); + ASSERT_EQ(subnets.size(), 1); + + mp::DNSMasqServer dns{data_dir.path(), subnets}; make_lease_entry(); - dns.release_mac(hw_addr); + dns.release_mac(hw_addr, subnets.front().first); EXPECT_TRUE(QFile::exists(dhcp_release_called)); EXPECT_TRUE(logger->logged_lines.size() > 0); @@ -170,10 +194,13 @@ TEST_F(DNSMasqServer, releaseMacCrashesLogsFailure) const QString dhcp_release_called{QDir{data_dir.path()}.filePath("dhcp_release_called")}; const std::string crash_hw_addr{"00:00:00:00:00:00"}; - mp::DNSMasqServer dns{data_dir.path(), dhcp_release_called, subnet}; + auto subnets = make_subnets(dhcp_release_called, default_subnet); + ASSERT_EQ(subnets.size(), 1); + + mp::DNSMasqServer dns{data_dir.path(), subnets}; make_lease_entry(crash_hw_addr); - dns.release_mac(crash_hw_addr); + dns.release_mac(crash_hw_addr, subnets.front().first); EXPECT_THAT(logger->logged_lines, Contains(fmt::format("failed to release ip addr {} with mac {}: Crashed", @@ -190,8 +217,9 @@ TEST_F(DNSMasqServer, dnsmasqStartsAndDoesNotThrow) TEST_F(DNSMasqServer, dnsmasqFailsAndThrows) { - EXPECT_THROW((mp::DNSMasqServer{data_dir.path(), bridge_name, error_subnet}), - std::runtime_error); + auto error_subnets = make_subnets(dummy_bridge, error_subnet); + ASSERT_EQ(error_subnets.size(), 1); + EXPECT_THROW((mp::DNSMasqServer{data_dir.path(), error_subnets}), std::runtime_error); } TEST_F(DNSMasqServer, dnsmasqCreatesConfFile) @@ -256,7 +284,7 @@ struct DNSMasqServerMockedProcess : public DNSMasqServer std::unique_ptr factory_scope = mpt::MockProcessFactory::Inject(); - inline static const auto exe = mp::DNSMasqProcessSpec{{}, {}, {}, {}}.program(); + inline static const auto exe = mp::DNSMasqProcessSpec{{}, {}, {}}.program(); }; TEST_F(DNSMasqServerMockedProcess, dnsmasqCheckSkipsStartIfAlreadyRunning) diff --git a/tests/qemu/linux/test_qemu_platform_detail.cpp b/tests/qemu/linux/test_qemu_platform_detail.cpp index a6fa2aeff4..ba9bf5a498 100644 --- a/tests/qemu/linux/test_qemu_platform_detail.cpp +++ b/tests/qemu/linux/test_qemu_platform_detail.cpp @@ -40,39 +40,74 @@ namespace { struct QemuPlatformDetail : public Test { - QemuPlatformDetail() - : mock_dnsmasq_server{std::make_unique()}, - mock_firewall_config{std::make_unique()} + QemuPlatformDetail() : mock_dnsmasq_server{std::make_unique()} { - EXPECT_CALL(*mock_backend, get_subnet(_, _)).WillOnce([this](auto...) { return subnet; }); - - EXPECT_CALL(*mock_dnsmasq_server_factory, make_dnsmasq_server(_, _, _)) - .WillOnce([this](auto...) { return std::move(mock_dnsmasq_server); }); - - EXPECT_CALL(*mock_firewall_config_factory, make_firewall_config(_, _)) - .WillOnce([this](auto...) { return std::move(mock_firewall_config); }); - - EXPECT_CALL(*mock_utils, run_cmd_for_status(QString("ip"), _, _)) - .WillRepeatedly(Return(true)); EXPECT_CALL(*mock_utils, run_cmd_for_status(QString("ip"), - QStringList({"addr", "show", multipass_bridge_name}), + Not(ElementsAre(QString("addr"), QString("show"), _)), _)) - .WillOnce(Return(false)) - .WillOnce(Return(true)); + .WillRepeatedly(Return(true)); + + for (const auto& vswitch : switches) + { + EXPECT_CALL(*mock_backend, get_subnet(_, vswitch.bridge_name)) + .WillOnce([subnet = vswitch.subnet](auto...) { return subnet; }); + + EXPECT_CALL(*mock_firewall_config_factory, + make_firewall_config(vswitch.bridge_name, vswitch.subnet)) + .WillOnce( + [this, &vswitch](auto...) { return std::move(vswitch.mock_firewall_config); }); + + EXPECT_CALL( + *mock_utils, + run_cmd_for_status( + QString("ip"), + ElementsAre(QString("addr"), QString("show"), QString(vswitch.bridge_name)), + _)) + .WillOnce(Return(false)) + .WillOnce(Return(true)); + } + + EXPECT_CALL(*mock_dnsmasq_server_factory, make_dnsmasq_server(_, _)) + .WillOnce([this](auto...) { return std::move(mock_dnsmasq_server); }); EXPECT_CALL(*mock_file_ops, open(_, _)).WillRepeatedly(Return(true)); EXPECT_CALL(*mock_file_ops, write(_, _)).WillRepeatedly(Return(1)); }; + struct Switch + { + QString bridge_name; + std::string hw_addr; + std::string subnet; + std::string name; + mutable std::unique_ptr mock_firewall_config; + + Switch(const QString& bridge_name, + const std::string& hw_addr, + const std::string& subnet, + const std::string& name) + : bridge_name(bridge_name), + hw_addr(hw_addr), + subnet(subnet), + name(name), + mock_firewall_config(std::make_unique()) + { + } + + Switch(const Switch& other) + : Switch(other.bridge_name, other.hw_addr, other.subnet, other.name) + { + } + }; + const std::vector switches{ + {"mpqemubrzone1", "52:54:00:6f:29:7e", "192.168.64", "foo"}, + {"mpqemubrzone2", "52:54:00:6f:29:7f", "192.168.96", "bar"}, + {"mpqemubrzone3", "52:54:00:6f:29:80", "192.168.128", "baz"}}; + mpt::TempDir data_dir; - const QString multipass_bridge_name{"mpqemubr0"}; - const std::string hw_addr{"52:54:00:6f:29:7e"}; - const std::string subnet{"192.168.64"}; - const std::string name{"foo"}; std::unique_ptr mock_dnsmasq_server; - std::unique_ptr mock_firewall_config; mpt::MockUtils::GuardedMock utils_attr{mpt::MockUtils::inject()}; mpt::MockUtils* mock_utils = utils_attr.first; @@ -96,69 +131,83 @@ struct QemuPlatformDetail : public Test }; } // namespace -TEST_F(QemuPlatformDetail, ctorSetsUpExpectedVirtualSwitch) +TEST_F(QemuPlatformDetail, ctorSetsUpExpectedVirtualSwitches) { - const QString qstring_subnet{QString::fromStdString(subnet)}; + for (const auto& vswitch : switches) + { + const QString qstring_subnet{QString::fromStdString(vswitch.subnet)}; - EXPECT_CALL(*mock_utils, - run_cmd_for_status(QString("ip"), - ElementsAre(QString("link"), - QString("add"), - multipass_bridge_name, - QString("address"), - _, - QString("type"), - QString("bridge")), - _)) - .WillOnce(Return(true)); - EXPECT_CALL(*mock_utils, - run_cmd_for_status(QString("ip"), - ElementsAre(QString("address"), - QString("add"), - QString("%1.1/24").arg(qstring_subnet), - QString("dev"), - multipass_bridge_name, - "broadcast", - QString("%1.255").arg(qstring_subnet)), - _)) - .WillOnce(Return(true)); - EXPECT_CALL( - *mock_utils, - run_cmd_for_status( - QString("ip"), - ElementsAre(QString("link"), QString("set"), multipass_bridge_name, QString("up")), - _)) - .WillOnce(Return(true)); + EXPECT_CALL(*mock_utils, + run_cmd_for_status(QString("ip"), + ElementsAre(QString("link"), + QString("add"), + vswitch.bridge_name, + QString("address"), + _, + QString("type"), + QString("bridge")), + _)) + .WillOnce(Return(true)); + EXPECT_CALL(*mock_utils, + run_cmd_for_status(QString("ip"), + ElementsAre(QString("address"), + QString("add"), + QString("%1.1/24").arg(qstring_subnet), + QString("dev"), + vswitch.bridge_name, + "broadcast", + QString("%1.255").arg(qstring_subnet)), + _)) + .WillOnce(Return(true)); + + EXPECT_CALL( + *mock_utils, + run_cmd_for_status( + QString("ip"), + ElementsAre(QString("link"), QString("set"), vswitch.bridge_name, QString("up")), + _)) + .WillOnce(Return(true)); + } mp::QemuPlatformDetail qemu_platform_detail{data_dir.path()}; } TEST_F(QemuPlatformDetail, getIpForReturnsExpectedInfo) { - const mp::IPAddress ip_address{fmt::format("{}.5", subnet)}; - - EXPECT_CALL(*mock_dnsmasq_server, get_ip_for(hw_addr)).WillOnce([&ip_address](auto...) { - return ip_address; - }); + for (const auto& vswitch : switches) + { + const mp::IPAddress ip_address{fmt::format("{}.5", vswitch.subnet)}; + EXPECT_CALL(*mock_dnsmasq_server, get_ip_for(vswitch.hw_addr)) + .WillOnce([ip = ip_address](auto...) { return ip; }); + } mp::QemuPlatformDetail qemu_platform_detail{data_dir.path()}; - auto addr = qemu_platform_detail.get_ip_for(hw_addr); + for (const auto& vswitch : switches) + { + const mp::IPAddress ip_address{fmt::format("{}.5", vswitch.subnet)}; + auto addr = qemu_platform_detail.get_ip_for(vswitch.hw_addr); - EXPECT_EQ(*addr, ip_address); + ASSERT_TRUE(addr.has_value()); + EXPECT_EQ(*addr, ip_address); + } } TEST_F(QemuPlatformDetail, platformArgsGenerateNetResourcesRemovesWorksAsExpected) { mp::VirtualMachineDescription vm_desc; mp::NetworkInterface extra_interface{"br-en0", "52:54:00:98:76:54", true}; - vm_desc.vm_name = "foo"; - vm_desc.default_mac_address = hw_addr; + + const auto& vswitch = switches.front(); + vm_desc.vm_name = vswitch.name; + vm_desc.zone = "zone1"; + vm_desc.default_mac_address = vswitch.hw_addr; vm_desc.extra_interfaces = {extra_interface}; QString tap_name; - EXPECT_CALL(*mock_dnsmasq_server, release_mac(hw_addr)).WillOnce(Return()); + EXPECT_CALL(*mock_dnsmasq_server, release_mac(vswitch.hw_addr, vswitch.bridge_name)) + .WillOnce(Return()); EXPECT_CALL( *mock_utils, @@ -209,7 +258,47 @@ TEST_F(QemuPlatformDetail, platformArgsGenerateNetResourcesRemovesWorksAsExpecte _)) .WillOnce(Return(true)); - qemu_platform_detail.remove_resources_for(name); + qemu_platform_detail.remove_resources_for(vswitch.name); +} + +TEST_F(QemuPlatformDetail, tapDevicesAreRemovedOnDestruction) +{ + mp::VirtualMachineDescription vm_desc; + mp::NetworkInterface extra_interface{"br-en0", "52:54:00:98:76:54", true}; + + const auto& vswitch = switches.front(); + vm_desc.vm_name = vswitch.name; + vm_desc.zone = "zone1"; + vm_desc.default_mac_address = vswitch.hw_addr; + vm_desc.extra_interfaces = {extra_interface}; + + QString tap_name; + + EXPECT_CALL( + *mock_utils, + run_cmd_for_status( + QString("ip"), + ElementsAre(QString("addr"), QString("show"), mpt::match_qstring(StartsWith("tap-"))), + _)) + .WillOnce([&tap_name](auto& cmd, auto& opts, auto...) { + tap_name = opts.last(); + return false; + }); + + mp::QemuPlatformDetail qemu_platform_detail{data_dir.path()}; + + const auto platform_args = qemu_platform_detail.vm_platform_args(vm_desc); + + EXPECT_CALL(*mock_utils, + run_cmd_for_status(QString("ip"), + ElementsAre(QString("addr"), QString("show"), tap_name), + _)) + .WillOnce(Return(true)); + EXPECT_CALL(*mock_utils, + run_cmd_for_status(QString("ip"), + ElementsAre(QString("link"), QString("delete"), tap_name), + _)) + .WillOnce(Return(true)); } TEST_F(QemuPlatformDetail, platformHealthCheckCallsExpectedMethods) @@ -217,7 +306,11 @@ TEST_F(QemuPlatformDetail, platformHealthCheckCallsExpectedMethods) EXPECT_CALL(*mock_backend, check_for_kvm_support()).WillOnce(Return()); EXPECT_CALL(*mock_backend, check_if_kvm_is_in_use()).WillOnce(Return()); EXPECT_CALL(*mock_dnsmasq_server, check_dnsmasq_running()).WillOnce(Return()); - EXPECT_CALL(*mock_firewall_config, verify_firewall_rules()).WillOnce(Return()); + + for (const auto& vswitch : switches) + { + EXPECT_CALL(*vswitch.mock_firewall_config, verify_firewall_rules()).WillOnce(Return()); + } mp::QemuPlatformDetail qemu_platform_detail{data_dir.path()};