Commit bcc073a7 authored by jan.koester's avatar jan.koester
Browse files

test

parent 3edb4418
Loading
Loading
Loading
Loading
+11 −2
Changes for include/sysbus/client.h: 11 added lines, 2 removed lines.
Original line number Diff line number Diff line
@@ -140,7 +140,7 @@ struct InterfaceInfo {
    bool adminUp = false;
    bool carrier = false;
    bool stopped = false;      // stopped on request: stays down
    std::vector<std::string> addresses;  // current, "a.b.c.d/len"
    std::vector<std::string> addresses;  // current, "a.b.c.d/len", IPv6 too
    // the configuration that applies (first matching .network file), if any
    std::string config;        // file name without ".network"
    std::string configPath;
@@ -148,6 +148,9 @@ struct InterfaceInfo {
    bool dhcp = false;
    std::string configAddress; // [Network] Address=
    std::string gateway;
    std::string configAddress6; // [Network] Address= (IPv6)
    std::string gateway6;
    std::string ipv6AcceptRA;  // [Network] IPv6AcceptRA=: yes, no, "" (the kernel's default)
    std::vector<std::string> dns;
    std::string kind;          // "bond", "bridge", "vlan", "wireguard", "veth" or "" (what the device is)
    std::string master;        // the bond/bridge it is a member of now
@@ -240,7 +243,10 @@ struct WireGuardKeys {
// that interface unless config/match say otherwise.
// Edit: changes only the fields that are set; the target is config if
// given, else the configuration that applies to interface. An empty
// address/gateway, or an empty dns list, clears it.
// address/gateway, or an empty dns list, clears it. address6/gateway6:
// IPv6, next to DHCP or an IPv4 address; ipv6AcceptRA: yes (router
// advertisements: default route, SLAAC address), no, "" (the kernel's
// default).
struct InterfaceSettings {
    std::string interface;
    std::string config;
@@ -248,6 +254,9 @@ struct InterfaceSettings {
    std::optional<bool> dhcp;
    std::optional<std::string> address;
    std::optional<std::string> gateway;
    std::optional<std::string> address6;
    std::optional<std::string> gateway6;
    std::optional<std::string> ipv6AcceptRA;
    std::optional<std::vector<std::string>> dns;
};

+85 −28
Changes for src/network.cpp: 85 added lines, 28 removed lines.
Original line number Diff line number Diff line
@@ -230,10 +230,14 @@ bool NetworkManager::parseNetworkFile(const std::string &path, NetworkConfig &cf
                cfg.dhcp = (val == "yes" || val == "ipv4" || val == "true");
            }
            else if (key == "Address") {
                cfg.address = val;
                (val.find(':') != std::string::npos ? cfg.address6 : cfg.address) = val;
            }
            else if (key == "Gateway") {
                cfg.gateway = val;
                (val.find(':') != std::string::npos ? cfg.gateway6 : cfg.gateway) = val;
            }
            else if (key == "IPv6AcceptRA") {
                cfg.ipv6AcceptRA = val == "yes" || val == "true" || val == "1" ? "yes"
                                 : val == "no" || val == "false" || val == "0" ? "no" : "";
            }
            else if (key == "DNS") {
                cfg.dns.push_back(val);
@@ -585,9 +589,9 @@ void NetworkManager::configureInterface(const std::string &ifname, const Network
            else
                std::cout << "[syslite/net] " << ifname << ": member of " << master << std::endl;
            // its addresses only now: until it is a port they still work
            flushIpv4Addresses(ifname);
            flushAddresses(ifname);
        } else if (currentMaster(ifname) != master) {
            flushIpv4Addresses(ifname);
            flushAddresses(ifname);
            // a bond takes only members that are down
            setLinkDown(ifname);
            if (!setMaster(ifname, master))
@@ -600,7 +604,8 @@ void NetworkManager::configureInterface(const std::string &ifname, const Network
        return;
    }

    // Bring interface up
    // Bring interface up (IPv6AcceptRA= first: SLAAC starts with the link)
    setIpv6Sysctls(ifname, cfg);
    if (cfg.linkUp)
        setLinkUp(ifname);

@@ -633,6 +638,19 @@ void NetworkManager::configureInterface(const std::string &ifname, const Network
        for (auto &d : cfg.dns)
            _dnsByIf[ifname].push_back(d);
    }
    // IPv6: static, next to DHCPv4 or an IPv4 address
    if (!cfg.address6.empty()) {
        if (addAddress(ifname, cfg.address6))
            std::cout << "[syslite/net] " << ifname << ": " << cfg.address6 << std::endl;
        else
            std::cerr << "[syslite/net] " << ifname << ": cannot add " << cfg.address6 << std::endl;
    }
    if (!cfg.gateway6.empty()) {
        if (addRoute(ifname, cfg.gateway6))
            std::cout << "[syslite/net] " << ifname << ": gw " << cfg.gateway6 << std::endl;
        else
            std::cerr << "[syslite/net] " << ifname << ": cannot add the route via " << cfg.gateway6 << std::endl;
    }
    // a WireGuard interface: the routes to its peers (after the addresses:
    // the last IPv4 address going takes the device's routes with it)
    if (const NetDevConfig *nd = findNetDev(ifname))
@@ -641,7 +659,7 @@ void NetworkManager::configureInterface(const std::string &ifname, const Network
}

void NetworkManager::unconfigureInterface(const std::string &ifname) {
    flushIpv4Addresses(ifname);
    flushAddresses(ifname);
    _dnsByIf.erase(ifname);
    _configured.erase(ifname);
}
@@ -844,6 +862,11 @@ static bool validConfigName(const std::string &name) {
    return true;
}

// addresses of its own: DHCP, static IPv4 or IPv6, or SLAAC
static bool hasAddresses(const NetworkConfig &cfg) {
    return cfg.dhcp || !cfg.address.empty() || !cfg.address6.empty() || cfg.ipv6AcceptRA == "yes";
}

// one value per line: a value may not smuggle in further keys or sections
static bool validValue(const std::string &v) {
    return v.find_first_of("\r\n") == std::string::npos;
@@ -888,7 +911,7 @@ static bool writeAtomically(const std::string &path, const std::string &text, st
bool NetworkManager::writeNetworkFile(const NetworkConfig &cfg, std::string *err) {
    if (!validConfigName(cfg.name)) { if (err) *err = "invalid configuration name '" + cfg.name + "'"; return false; }
    if (!validValue(cfg.matchName) || !validValue(cfg.address) || !validValue(cfg.gateway) ||
        !validValue(cfg.bond) || !validValue(cfg.bridge)) {
        !validValue(cfg.address6) || !validValue(cfg.gateway6) || !validValue(cfg.bond) || !validValue(cfg.bridge)) {
        if (err) *err = "values must be single lines";
        return false;
    }
@@ -911,6 +934,9 @@ bool NetworkManager::writeNetworkFile(const NetworkConfig &cfg, std::string *err
          << "DHCP=" << (cfg.dhcp ? "yes" : "no") << "\n";
        if (!cfg.address.empty()) f << "Address=" << cfg.address << "\n";
        if (!cfg.gateway.empty()) f << "Gateway=" << cfg.gateway << "\n";
        if (!cfg.address6.empty()) f << "Address=" << cfg.address6 << "\n";
        if (!cfg.gateway6.empty()) f << "Gateway=" << cfg.gateway6 << "\n";
        if (!cfg.ipv6AcceptRA.empty()) f << "IPv6AcceptRA=" << cfg.ipv6AcceptRA << "\n";
        for (auto &d : cfg.dns) f << "DNS=" << d << "\n";
        if (!cfg.bond.empty()) f << "Bond=" << cfg.bond << "\n";
        if (!cfg.bridge.empty()) f << "Bridge=" << cfg.bridge << "\n";
@@ -1171,7 +1197,7 @@ bool NetworkManager::saveNetDev(const NetDevConfig &in, const std::vector<std::s
        for (auto &m : members) {
            if (std::find(before.begin(), before.end(), m) != before.end()) continue;
            const NetworkConfig *own = ownConfigOf(m);
            if (!own || own->filePath.rfind(base, 0) != 0 || (!own->dhcp && own->address.empty())) continue;
            if (!own || own->filePath.rfind(base, 0) != 0 || !hasAddresses(*own)) continue;
            NetworkConfig moved = *own;
            moved.name = memberConfigName(nd.ifname);
            moved.matchName = nd.ifname;
@@ -1190,6 +1216,9 @@ bool NetworkManager::saveNetDev(const NetDevConfig &in, const std::vector<std::s
        cfg.dhcp = false;
        cfg.address.clear();
        cfg.gateway.clear();
        cfg.address6.clear();
        cfg.gateway6.clear();
        cfg.ipv6AcceptRA.clear();
        cfg.dns.clear();
        cfg.bond = nd.kind == "bond" ? nd.ifname : "";
        cfg.bridge = nd.kind == "bridge" ? nd.ifname : "";
@@ -1246,7 +1275,8 @@ bool NetworkManager::writeNetDevFile(const NetDevConfig &nd, std::string *err) {
// else in it): once its last VLAN is gone, it goes too
static bool onlyCarriedVlans(const NetworkConfig &cfg) {
    return cfg.vlans.empty() && cfg.name == "10-" + cfg.matchName && !cfg.dhcp && cfg.address.empty() &&
           cfg.gateway.empty() && cfg.dns.empty() && cfg.bond.empty() && cfg.bridge.empty();
           cfg.gateway.empty() && cfg.address6.empty() && cfg.gateway6.empty() && cfg.ipv6AcceptRA.empty() &&
           cfg.dns.empty() && cfg.bond.empty() && cfg.bridge.empty();
}

// VLAN=<nd.ifname> in the own configuration of nd.parent (10-<parent>: a
@@ -1310,7 +1340,7 @@ bool NetworkManager::deleteNetDev(const std::string &ifname, std::string *err) {
    const NetworkConfig *own = nullptr;
    for (auto &c : _configs)
        if (c.matchName == ifname && c.bond.empty() && c.bridge.empty() && c.filePath.rfind(base, 0) == 0) { own = &c; break; }
    const bool giveBack = members.size() == 1 && own && (own->dhcp || !own->address.empty());
    const bool giveBack = members.size() == 1 && own && hasAddresses(*own);
    const NetworkConfig ownCopy = own ? *own : NetworkConfig{};

    std::error_code ec;
@@ -1697,16 +1727,20 @@ std::vector<std::string> NetworkManager::listAddresses(const std::string &ifname
    return out;
}

void NetworkManager::flushIpv4Addresses(const std::string &ifname) {
void NetworkManager::flushAddresses(const std::string &ifname) {
    int idx = getIfIndex(ifname);
    if (idx < 0) return;
    struct Addr { struct ifaddrmsg ifa; uint32_t local, address; };
    struct Addr { struct ifaddrmsg ifa; uint8_t local[16], address[16]; };
    std::vector<Addr> addrs;
    forEachAddress(idx, [&](const struct ifaddrmsg &ifa, const void *local, const void *address) {
        if (ifa.ifa_family != AF_INET || !local) return;
        Addr a{ifa, 0, 0};
        std::memcpy(&a.local, local, 4);
        std::memcpy(&a.address, address ? address : local, 4);
        // IPv6: the global ones (static, SLAAC); the link-local one belongs
        // to the link
        if (!local || (ifa.ifa_family != AF_INET && (ifa.ifa_family != AF_INET6 || ifa.ifa_scope != RT_SCOPE_UNIVERSE)))
            return;
        Addr a{ifa, {0}, {0}};
        const size_t len = ifa.ifa_family == AF_INET ? 4 : 16;
        std::memcpy(a.local, local, len);
        std::memcpy(a.address, address ? address : local, len);
        addrs.push_back(a);
    });
    for (auto &a : addrs) {
@@ -1719,13 +1753,31 @@ void NetworkManager::flushIpv4Addresses(const std::string &ifname) {
        msg.hdr.nlmsg_seq = 1;
        auto *ifa = reinterpret_cast<struct ifaddrmsg *>(NLMSG_DATA(&msg.hdr));
        *ifa = a.ifa;
        nlAddAttr(&msg.hdr, sizeof(msg), IFA_LOCAL, &a.local, 4);
        nlAddAttr(&msg.hdr, sizeof(msg), IFA_ADDRESS, &a.address, 4);
        const int len = a.ifa.ifa_family == AF_INET ? 4 : 16;
        nlAddAttr(&msg.hdr, sizeof(msg), IFA_LOCAL, a.local, len);
        nlAddAttr(&msg.hdr, sizeof(msg), IFA_ADDRESS, a.address, len);
        netlinkSend(fd, &msg.hdr);
        close(fd);
    }
}

// ─── IPv6 sysctls ───────────────────────────────────────────────────────────

void NetworkManager::setIpv6Sysctls(const std::string &ifname, const NetworkConfig &cfg) {
    if (cfg.ipv6AcceptRA.empty()) return;
    const std::string conf = "/proc/sys/net/ipv6/conf/" + ifname + "/";
    auto set = [&](const char *key, const char *value) {
        int fd = ::open((conf + key).c_str(), O_WRONLY | O_CLOEXEC);
        bool ok = fd >= 0 && ::write(fd, value, std::strlen(value)) == static_cast<ssize_t>(std::strlen(value));
        if (fd >= 0) ::close(fd);
        if (!ok) std::cerr << "[syslite/net] " << ifname << ": cannot set " << conf << key << std::endl;
    };
    // 2: also with forwarding on (a host with a bridge for containers)
    const bool on = cfg.ipv6AcceptRA == "yes";
    set("accept_ra", on ? "2" : "0");
    set("autoconf", on ? "1" : "0");
}

// ─── Netlink: set link up ───────────────────────────────────────────────────

bool NetworkManager::setLinkUp(const std::string &ifname) {
@@ -1782,15 +1834,17 @@ bool NetworkManager::addAddress(const std::string &ifname, const std::string &ad
    int idx = getIfIndex(ifname);
    if (idx < 0) return false;

    // Parse "192.168.1.10/24"
    // Parse "192.168.1.10/24" or "2001:db8::10/64"
    auto slash = addrCidr.find('/');
    if (slash == std::string::npos) return false;

    std::string ipStr = addrCidr.substr(0, slash);
    int prefixLen = std::atoi(addrCidr.substr(slash + 1).c_str());

    uint32_t ip = parseIpv4(ipStr);
    if (ip == 0 && ipStr != "0.0.0.0") return false;
    const bool v6 = ipStr.find(':') != std::string::npos;
    uint8_t ip[16] = {0};
    if (inet_pton(v6 ? AF_INET6 : AF_INET, ipStr.c_str(), ip) != 1) return false;
    if (prefixLen < 0 || prefixLen > (v6 ? 128 : 32)) return false;

    int fd = netlinkOpen();
    if (fd < 0) return false;
@@ -1802,13 +1856,13 @@ bool NetworkManager::addAddress(const std::string &ifname, const std::string &ad
    msg.hdr.nlmsg_seq = 1;

    auto *ifa = reinterpret_cast<struct ifaddrmsg *>(NLMSG_DATA(&msg.hdr));
    ifa->ifa_family = AF_INET;
    ifa->ifa_family = v6 ? AF_INET6 : AF_INET;
    ifa->ifa_prefixlen = prefixLen;
    ifa->ifa_index = idx;
    ifa->ifa_scope = RT_SCOPE_UNIVERSE;

    nlAddAttr(&msg.hdr, sizeof(msg), IFA_LOCAL, &ip, 4);
    nlAddAttr(&msg.hdr, sizeof(msg), IFA_ADDRESS, &ip, 4);
    nlAddAttr(&msg.hdr, sizeof(msg), IFA_LOCAL, ip, v6 ? 16 : 4);
    nlAddAttr(&msg.hdr, sizeof(msg), IFA_ADDRESS, ip, v6 ? 16 : 4);

    bool ok = netlinkSend(fd, &msg.hdr);
    close(fd);
@@ -1857,8 +1911,11 @@ bool NetworkManager::addRoute(const std::string &ifname, const std::string &gate
    int idx = getIfIndex(ifname);
    if (idx < 0) return false;

    uint32_t gw = parseIpv4(gateway);
    if (gw == 0) return false;
    // IPv4 or IPv6 (a link-local one too: the route names the interface)
    const bool v6 = gateway.find(':') != std::string::npos;
    uint8_t gw[16] = {0};
    if (inet_pton(v6 ? AF_INET6 : AF_INET, gateway.c_str(), gw) != 1) return false;
    if (!v6 && parseIpv4(gateway) == 0) return false;

    int fd = netlinkOpen();
    if (fd < 0) return false;
@@ -1870,7 +1927,7 @@ bool NetworkManager::addRoute(const std::string &ifname, const std::string &gate
    msg.hdr.nlmsg_seq = 1;

    auto *rtm = reinterpret_cast<struct rtmsg *>(NLMSG_DATA(&msg.hdr));
    rtm->rtm_family = AF_INET;
    rtm->rtm_family = v6 ? AF_INET6 : AF_INET;
    rtm->rtm_dst_len = 0;       // default route = /0
    rtm->rtm_src_len = 0;
    rtm->rtm_table = RT_TABLE_MAIN;
@@ -1878,7 +1935,7 @@ bool NetworkManager::addRoute(const std::string &ifname, const std::string &gate
    rtm->rtm_scope = RT_SCOPE_UNIVERSE;
    rtm->rtm_type = RTN_UNICAST;

    nlAddAttr(&msg.hdr, sizeof(msg), RTA_GATEWAY, &gw, 4);
    nlAddAttr(&msg.hdr, sizeof(msg), RTA_GATEWAY, gw, v6 ? 16 : 4);
    int oifIdx = idx;
    nlAddAttr(&msg.hdr, sizeof(msg), RTA_OIF, &oifIdx, 4);

+13 −3
Changes for src/network.h: 13 added lines, 3 removed lines.
Original line number Diff line number Diff line
@@ -20,6 +20,13 @@ namespace syslite {
        bool        dhcp = false;    // DHCP=yes/ipv4
        std::string address;         // Address=192.168.1.10/24
        std::string gateway;         // Gateway=192.168.1.1
        // Address=/Gateway= once more for IPv6 (told apart by the ':')
        std::string address6;        // Address=2001:db8::10/64
        std::string gateway6;        // Gateway=fe80::1 (link-local or global)
        // IPv6AcceptRA=yes|no: router advertisements (default routes and
        // SLAAC addresses), also with forwarding on; "": the kernel's
        // default (accept_ra and autoconf as they are)
        std::string ipv6AcceptRA;
        std::vector<std::string> dns; // DNS=8.8.8.8

        // [Network] Bond=/Bridge=: the interface is a member of that bond or
@@ -258,12 +265,15 @@ namespace syslite {
        // Apply cfg to one interface (link up, DHCP or static address/route,
        // DNS servers); records it as configured
        void configureInterface(const std::string &ifname, const NetworkConfig &cfg);
        // Undo it: delete its IPv4 addresses (and with them its routes), drop
        // Undo it: delete its addresses (and with them its routes), drop
        // its DNS servers
        void unconfigureInterface(const std::string &ifname);
        // Netlink: the interface's addresses; delete its IPv4 addresses
        // Netlink: the interface's addresses; delete its IPv4 and global
        // IPv6 addresses (the IPv6 link-local one stays)
        std::vector<std::string> listAddresses(const std::string &ifname);
        void flushIpv4Addresses(const std::string &ifname);
        void flushAddresses(const std::string &ifname);
        // the IPv6 sysctls of cfg (IPv6AcceptRA=): before the link goes up
        void setIpv6Sysctls(const std::string &ifname, const NetworkConfig &cfg);

        // Helper: get interface index
        int getIfIndex(const std::string &ifname);
+6 −0
Changes for src/sysbus/client.cpp: 6 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -71,6 +71,9 @@ InterfaceInfo interfaceFromFields(const std::vector<std::pair<std::string, std::
        else if (k == "dhcp") i.dhcp = v == "yes";
        else if (k == "config_address") i.configAddress = v;
        else if (k == "gateway") i.gateway = v;
        else if (k == "config_address6") i.configAddress6 = v;
        else if (k == "gateway6") i.gateway6 = v;
        else if (k == "ipv6_accept_ra") i.ipv6AcceptRA = v;
        else if (k == "dns") i.dns.push_back(v);
        else if (k == "kind") i.kind = v;
        else if (k == "master") i.master = v;
@@ -318,6 +321,9 @@ static std::vector<std::pair<std::string, std::string>> settingsArgs(const Inter
    if (s.dhcp) a.emplace_back("dhcp", *s.dhcp ? "yes" : "no");
    if (s.address) a.emplace_back("address", *s.address);
    if (s.gateway) a.emplace_back("gateway", *s.gateway);
    if (s.address6) a.emplace_back("address6", *s.address6);
    if (s.gateway6) a.emplace_back("gateway6", *s.gateway6);
    if (s.ipv6AcceptRA) a.emplace_back("ipv6_accept_ra", *s.ipv6AcceptRA);
    if (s.dns) {
        a.emplace_back("set_dns", "yes");  // also when the list is empty: clears it
        for (auto &d : *s.dns) a.emplace_back("dns", d);
+36 −1
Changes for src/syslite.cpp: 36 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -10,8 +10,10 @@
#include <filesystem>
#include <cstring>
#include <csignal>
#include <cstdlib>

#include <unistd.h>
#include <arpa/inet.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/mount.h>
@@ -379,6 +381,31 @@ namespace syslite {
        return r;
    }

    // what is wrong with cfg's addresses ("" if nothing): Address= and
    // Gateway= per address family, IPv6AcceptRA= yes, no or unset
    static std::string checkAddresses(const NetworkConfig &cfg) {
        auto ip = [](int fam, const std::string &s) {
            unsigned char buf[16];
            return inet_pton(fam, s.c_str(), buf) == 1;
        };
        auto cidr = [&](int fam, const std::string &s) {
            size_t slash = s.find('/');
            if (slash == std::string::npos || !ip(fam, s.substr(0, slash))) return false;
            std::string len = s.substr(slash + 1);
            if (len.empty() || len.size() > 3 || len.find_first_not_of("0123456789") != std::string::npos) return false;
            return std::atoi(len.c_str()) <= (fam == AF_INET ? 32 : 128);
        };
        if (!cfg.address.empty() && !cidr(AF_INET, cfg.address))
            return "invalid address '" + cfg.address + "' (a.b.c.d/len)";
        if (!cfg.gateway.empty() && !ip(AF_INET, cfg.gateway)) return "invalid gateway '" + cfg.gateway + "'";
        if (!cfg.address6.empty() && !cidr(AF_INET6, cfg.address6))
            return "invalid IPv6 address '" + cfg.address6 + "' (2001:db8::10/64)";
        if (!cfg.gateway6.empty() && !ip(AF_INET6, cfg.gateway6)) return "invalid IPv6 gateway '" + cfg.gateway6 + "'";
        if (!cfg.ipv6AcceptRA.empty() && cfg.ipv6AcceptRA != "yes" && cfg.ipv6AcceptRA != "no")
            return "ipv6_accept_ra takes yes, no or nothing (the kernel's default)";
        return "";
    }

    static void addInterfaceFields(sysbus::Message &m, const InterfaceStatus &st) {
        m.add("interface", st.name);
        m.add("mac", st.mac);
@@ -397,6 +424,9 @@ namespace syslite {
            m.add("dhcp", cfg->dhcp ? "yes" : "no");
            m.add("config_address", cfg->address);
            m.add("gateway", cfg->gateway);
            m.add("config_address6", cfg->address6);
            m.add("gateway6", cfg->gateway6);
            m.add("ipv6_accept_ra", cfg->ipv6AcceptRA);
            for (auto &d : cfg->dns) m.add("dns", d);
            if (!cfg->bond.empty()) m.add("member_of", cfg->bond);
            else if (!cfg->bridge.empty()) m.add("member_of", cfg->bridge);
@@ -478,12 +508,17 @@ namespace syslite {
            if (call.has("dhcp")) cfg.dhcp = call.get("dhcp") == "yes";
            if (call.has("address")) cfg.address = call.get("address");
            if (call.has("gateway")) cfg.gateway = call.get("gateway");
            if (call.has("address6")) cfg.address6 = call.get("address6");
            if (call.has("gateway6")) cfg.gateway6 = call.get("gateway6");
            if (call.has("ipv6_accept_ra")) cfg.ipv6AcceptRA = call.get("ipv6_accept_ra");
            if (call.has("set_dns")) cfg.dns = call.getAll("dns");
            if (cfg.matchName.empty())
                return Message::error(call, err::InvalidArgs, "a configuration needs a match (interface name or pattern)");
            if (std::string bad = checkAddresses(cfg); !bad.empty())
                return Message::error(call, err::InvalidArgs, bad);
            // without addresses only what carries others: a VLAN parent, a
            // bond/bridge/VLAN (layer 2 only, e.g. for containers)
            if (!cfg.dhcp && cfg.address.empty() && cfg.bond.empty() && cfg.bridge.empty() && cfg.vlans.empty() &&
            if (!cfg.dhcp && cfg.address.empty() && cfg.address6.empty() && cfg.ipv6AcceptRA != "yes" && cfg.bond.empty() && cfg.bridge.empty() && cfg.vlans.empty() &&
                !_network.findNetDev(cfg.matchName))
                return Message::error(call, err::InvalidArgs, "a configuration without DHCP needs an address");
            return done(_network.saveConfig(cfg, &why), {});
Loading