DEV Community

Cover image for Manage Ubuntu Linux Networking Programmatically with Netplan and D-Bus in C/C++
Mehmet YILMAZ
Mehmet YILMAZ

Posted on

Manage Ubuntu Linux Networking Programmatically with Netplan and D-Bus in C/C++

When we need to manage network adapter settings programmatically, we often encounter the challenge of using system calls to override static configurations. These attempts frequently lead to panic and crashes in the network services. At best, we might temporarily achieve our objectives until the next reboot, and even then, a correct implementation is rare.

If DHCP enters the picture, things become even more complicated. In addition to handling other complexities, we might need to implement or interact with a DHCP client.

As we've all seen, getting sidetracked by unrelated and complex issues during development often leads to major setbacks and failures.

In light of these challenges, we need to consider alternative approaches. Given that network management is operated by OS-provided services, we should aim to communicate correctly with these services instead of attempting to take over their roles.

https://github.com/mehmet-yilmaz/netplan-dbus-example


PS: If you work on a remote host, remember that changing the network settings may cause the connection lost. Do not apply any changes to your active connection interface if you are not sure what are you doing.


How Do OS Services Communicate with Each Other?

Inter-Process Communication

Operating systems consist of multiple services and processes that need to work together smoothly. IPC is the communication layer that allows these services to exchange data and synchronize actions effectively.

What is D-Bus?

D-Bus (Desktop Bus) is a message-oriented bus system and Remote Procedure Call (RPC) mechanism in Linux that enables communication between different processes on the same machine. It is designed to be simple and lightweight, suitable for both desktop and embedded environments.

Key Features of D-Bus:

  • Message Passing: Processes can send and receive messages, including signals, method calls, and return values.
  • Service Registration: Processes can register services on the bus, making functionalities available to others.
  • Introspection: D-Bus supports dynamic discovery of services and their capabilities.
  • Security: Includes mechanisms for authenticating and authorizing messages.
  • Efficiency: Designed to be efficient in both performance and resource usage.
  • Using Netplan on Ubuntu
  • For this article, we will use Ubuntu Server 22.04 LTS and its default network configuration service, Netplan. However, you can choose any OS and network service that provides a D-Bus API.

What is Netplan?

Netplan is a tool that provides a high-level configuration interface for network settings using YAML files. It abstracts the network configuration by communicating with supported network services like NetworkManager and systemd-networkd.

For more information: Netplan Documentation

Netplan also provides a simple D-Bus API and a CLI example for communication with the Netplan service over D-Bus:

Netplan D-Bus API
CLI Example

How to use D-Bus configuration API

See also: Netplan D-Bus reference, busctl reference. Copy the current state from/{etc,run,lib}/netplan/*.yaml by…
netplan.readthedocs.io

DBus CLI Example:

Before diving in, please try the following CLI example to get the current configuration for ensuring that all services working correctly.

busctl is a DBus communication CLI tool already installed in the Ubuntu system, there are some other alternatives that you can install if needed.

We will use the following command to call the Config() method, which starts a session and returns the Netplan-DBus configuration path.

Netplan-DBus service runs on the System Session, so you will need sudo privileges to communicate with it.

sudo busctl call io.netplan.Netplan /io/netplan/Netplan io.netplan.Netplan Config
Enter fullscreen mode Exit fullscreen mode

Expected output:

o "/io/netplan/Netplan/config/ULJIU0"
Enter fullscreen mode Exit fullscreen mode

o represents the output type OBJECT_PATH and the ”/io/netplan/Netplan/config/ULJIU0" as value.

OBJECT_PATH value is a session-based dynamic value, we will do all the configuration with this path during the session, but it will be changed on the next session after the timeout or if you call the Config method again.

Now we can call the Get() method to this configuration path as busctl call io.netplan.Netplan [ConfigurationPath] io.netplan.Netplan.Config Get to read the current configuration as examples here.

sudo busctl call io.netplan.Netplan /io/netplan/Netplan/config/ULJIU0 io.netplan.Netplan.Config Get
Enter fullscreen mode Exit fullscreen mode

Expected output depends on your existing network configuration differs:

s "network:\n  ethernets:\n    eth0:\n      dhcp4: true\n  renderer: networkd\n  version: 2\n"
Enter fullscreen mode Exit fullscreen mode

s represents the output type STRING and the value is the string parse output of the Netplans .yaml current configuration file.

Now we can move to the programming.

PS: If you work on a remote host, remember that changing the network settings may cause the connection lost. Do not apply any changes to your active connection interface if you are not sure what are you doing.

Lets Coding…

After this introduction, we can focus on implementing this in our C++ Program.

Dependencies

There are different DBus libraries you can choose from. I prefer the dbus-cxx library because it provides the native C++ implementation with Smart pointers support.

Please take a look at the project: https://dbus-cxx.github.io

Installation of the dbus-cxx library on Ubuntu 22.04 LTS.

Install the pre-requirements

  • ≥ C++17
  • cmake(≥3.8)
  • make
  • g++
  • libsig++(≥3.0)
  • expat
  • libdbus
sudo apt install build-essentials cmake libdbus-1-dev libexpat1-dev

git clone https://github.com/dbus-cxx/libsigc--3.0.git
cd libsigc--3.0
cd build
cmake ..
make install
Enter fullscreen mode Exit fullscreen mode

Please refer to the documentation for your operation system.

dbus-cxx: dbus-cxx Library - dbus-cxx.github.io
Do not forget to link the library dbus-cxx to your compiler with -ldbus-cxx flag for using it.

Finally, we are ready to hands-on.

I will create a service class that will interface the Netplan-DBus API and a basic CLI to interact with.

Project Structure

netplan-dbus-example/
├─ netplan.service.hpp
├─ cli.class.hpp
├─ main.cpp
Enter fullscreen mode Exit fullscreen mode

First of all, I will create the netplan.service.hpp file

#ifndef ___NETPLAN_DBUS_SERVICE_HPP___
#define ___NETPLAN_DBUS_SERVICE_HPP___

#include <memory>
#include <string>
#include <dbus-cxx-2.0/dbus-cxx.h>
#include <dbus-cxx-2.0/dbus-cxx/path.h>
#include <dbus-cxx-2.0/dbus-cxx/dispatcher.h>
#include <dbus-cxx-2.0/dbus-cxx/connection.h>
#include <dbus-cxx-2.0/dbus-cxx/objectproxy.h>
#include <dbus-cxx-2.0/dbus-cxx/methodproxybase.h>

class NetplanService
{
public:
    NetplanService()
    {
        this->m_path.clear(); // Ensure that it will return "True" when we call ".empty()" method as default to start a new session!
        this->setConfigurationFile();
        this->connect();
    };

    void connect()
    {
        try
        {
            this->m_dispatcher = DBus::StandaloneDispatcher::create();

            // Netplan DBus service runs on the System Bus.
            // For connecting to Netplan, we have to create a connection in the System Bus
            // !!! IMPORTANT: System Bus requires ROOT Privilages!!!
            this->m_connection = this->m_dispatcher->create_connection(DBus::BusType::SYSTEM);
            this->m_object_proxy = this->m_connection->create_object_proxy("io.netplan.Netplan", "/io/netplan/Netplan");

            this->m_path.clear(); // Ensure that it will return "True" when we call ".empty()" method as default to start a new session!
        }
        catch (const DBus::Error &err)
        {
            this->handleDBusError(err);
        }
    };

    // Confguration .yaml file will be stored and processed with this filename under the default netplan configuration folder.
    void setConfigurationFile(const char *filename = "dbus-test-config")
    {
        this->m_filename = filename;
    };

    const std::string configurationPath()
    {
        if (this->m_path.empty())
            this->initConfigurationPath();
        return this->m_path;
    };

    const std::string getConfiguration()
    {
        try
        {
            const std::string path = this->configurationPath();
            this->m_object_proxy->set_path(path);
            DBus::MethodProxy<std::string()> &Get = *(this->m_object_proxy->create_method<std::string()>("io.netplan.Netplan.Config", "Get"));
            return Get();
        }
        catch (const DBus::Error &err)
        {
            this->handleDBusError(err);
        }
    };

    const bool setConfiguration(const std::string &target, const std::string &value)
    {
        try
        {
            const std::string path = this->configurationPath();
            this->m_object_proxy->set_path(path);
            std::string config = target + "=" + value;
            DBus::MethodProxy<bool(std::string, std::string)> &Set = *(this->m_object_proxy->create_method<bool(std::string, std::string)>("io.netplan.Netplan.Config", "Set"));
            return Set(config, this->m_filename);
        }
        catch (const DBus::Error &err)
        {
            this->handleDBusError(err);
        }
    };
    bool tryConfiguration(const uint32_t &timeout)
    {
        try
        {
            const std::string path = this->configurationPath();
            this->m_object_proxy->set_path(path);
            DBus::MethodProxy<bool(uint32_t)> &Try = *(this->m_object_proxy->create_method<bool(uint32_t)>("io.netplan.Netplan.Config", "Try"));
            return Try(timeout);
        }
        catch (DBus::Error &err)
        {
            this->handleDBusError(err);
        };
    };
    bool applyConfiguration()
    {
        try
        {
            const std::string path = this->configurationPath();
            this->m_object_proxy->set_path(path);
            DBus::MethodProxy<bool()> &Apply = *(this->m_object_proxy->create_method<bool()>("io.netplan.Netplan.Config", "Apply"));
            return Apply();
        }
        catch (DBus::Error &err)
        {
            this->handleDBusError(err);
        };
    };

    bool cancelConfiguration()
    {
        try
        {
            const std::string path = this->configurationPath();
            this->m_object_proxy->set_path(path);
            DBus::MethodProxy<bool()> &Cancel = *(this->m_object_proxy->create_method<bool()>("io.netplan.Netplan.Config", "Cancel"));
            const bool result = Cancel();
            this->m_path.clear();
            return result;
        }
        catch (DBus::Error &err)
        {
            this->handleDBusError(err);
        };
    };

private:
    std::shared_ptr<DBus::Dispatcher> m_dispatcher{nullptr};
    std::shared_ptr<DBus::Connection> m_connection{nullptr};
    std::shared_ptr<DBus::ObjectProxy> m_object_proxy{nullptr};
    std::string m_path{NULL};
    std::string m_filename{NULL};

    void initConfigurationPath()
    {
        try
        {
            // Ensure that object path corrected!
            this->m_object_proxy->set_path("/io/netplan/Netplan");
            // Create a Method Proxy to represent Config Method of the Netplan-DBus API
            DBus::MethodProxy<DBus::Path()> &Config = *(this->m_object_proxy->create_method<DBus::Path()>("io.netplan.Netplan", "Config"));
            // Run the proxy method to start a new Session and get the Configuration path as a return value and assing it to the m_configuratoin_path
            this->m_path = Config();
        }
        catch (const DBus::Error &err)
        {
            this->handleDBusError(err);
        }
    };
    void handleDBusError(const DBus::Error &err)
    {
        // Reset the session in case of error
        this->m_path.clear();

        // Print out the error.
        printf("ERROR \n\tDBus Error: %s \n\tType: %s \n\tMessage: %s\n", err.name().c_str(), err.what().c_str(), err.message().c_str());
    };
};

#endif // !___NETPLAN_DBUS_SERVICE_HPP___
Enter fullscreen mode Exit fullscreen mode

then the cli.hpp file;

#ifndef ___CLI_HPP___
#define ___CLI_HPP___

#include <iostream>
#include <stdio.h>
#include "netplan.service.hpp"

class Cli
{
public:
    Cli() {};
    void run()
    {
        this->m_running = true;
        while (this->m_running)
        {
            this->printmenu();
        }
    };
    void printmenu()
    {
        printf("\n------------MENU-------------\n");
        for (const auto &item : menu)
        {
            printf("\n%c \t %s", item.first, item.second.c_str());
        }
        printf("\nPlease Enter Your Selection: ");
        this->readmenu();
    };

    void readmenu()
    {
        char c;
        std::cin >> c;
        switch (c)
        {
        case 'P':
            this->configurationPath();
            break;
        case 'G':
            this->getConfiguration();
            break;
        case 'S':
            this->setMenu();
            break;
        case 'Q':
            this->stop();
            break;
        default:
            break;
        }
    };

private:
    std::atomic_bool m_running = true;
    std::unique_ptr<NetplanService>
        netplanService = std::make_unique<NetplanService>();
    std::map<const char, const std::string> menu{
        {'P', "Configuration Path"},
        {'G', "Get Configuration"},
        {'S', "Set Configuration"},
        {'Q', "Quit"}};

    void stop()
    {
        this->m_running = false;
    };

    void configurationPath()
    {
        const std::string path = this->netplanService->configurationPath();
        printf("\n\t\tConfigiration Path: %s", path.c_str());
    };

    void getConfiguration()
    {
        const std::string configuration = this->netplanService->getConfiguration();
        printf("\nConfiguration\n%s\n", configuration.c_str());
    };

    void setConfiguration(const std::string &target, const std::string &value)
    {
        this->netplanService->setConfiguration(target, value);
    };

    void setMenu()
    {
        printf("\n------------------");
        this->getConfiguration();
        printf("\n\tSet Configuration");
        printf("\n\tEnter the configuration target: ");
        std::string target;
        std::cin >> target;
        printf("\n\tEnter the configuration value: ");
        std::string value;
        std::cin >> value;
        this->setConfiguration(target, value);
    };

protected:
};

#endif // !___CLI_HPP___
Enter fullscreen mode Exit fullscreen mode

and the main.cpp file

#include "cli.hpp"
int main(int argc, char *argv[])
{

    const auto cli = std::make_unique<Cli>();
    if (cli)
        cli->run();
    return 0;
}
Enter fullscreen mode Exit fullscreen mode

Since the Netplan runs on the System Session, we have to run our application with sudo privileges

Running Application Output:

------------MENU-------------

G        Get Configuration
P        Configuration Path
Q        Quit
S        Set Configuration
Please Enter Your Selection: 
Enter fullscreen mode Exit fullscreen mode

Configuration Path Selection:

------------MENU-------------

G        Get Configuration
P        Configuration Path
Q        Quit
S        Set Configuration
Please Enter Your Selection: P

                Configiration Path: /io/netplan/Netplan/config/EF2OR2
Enter fullscreen mode Exit fullscreen mode

Get Configuration Selection:

------------MENU-------------

G        Get Configuration
P        Configuration Path
Q        Quit
S        Set Configuration
Please Enter Your Selection: G

Configuration
network:
  version: 2
  ethernets:
    enxf8e43b893689:
      dhcp4: true
    eno1:
      dhcp4: true
Enter fullscreen mode Exit fullscreen mode

Set Configuration:

------------MENU-------------

G        Get Configuration
P        Configuration Path
Q        Quit
S        Set Configuration
Please Enter Your Selection: S

------------------
Configuration
network:
  version: 2
  ethernets:
    enxf8e43b893689:
      dhcp4: true
    eno1:
      dhcp4: true


        Set Configuration
        Enter the configuration target: 

Enter fullscreen mode Exit fullscreen mode

Here we can test and change the interface.

I have plug a USB-ETH adaptor for testing, if you are not sure which interface you are working on, do not try this.

ethernets.enxf8e43b893689.dhcp4 value to false .

------------MENU-------------

G        Get Configuration
P        Configuration Path
Q        Quit
S        Set Configuration
Please Enter Your Selection: S

------------------
Configuration
network:
  version: 2
  ethernets:
    enxf8e43b893689:
      dhcp4: true
    eno1:
      dhcp4: true


        Set Configuration
        Enter the configuration target: ethernets.enxf8e43b893689.dhcp4

        Enter the configuration value: false
Enter fullscreen mode Exit fullscreen mode

It will change the interface enxf8e43b893689.dhcp4 to false but since we do not apply this. It will not affect anything in this application.

Complete example could be inspected on github.
mehmet-yilmaz/netplan-dbus-example

This was all, you can implement your requirements and communicate with Netplan or any other service over DBus.

I hope this helps you.

Top comments (0)