1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413
|
D-Bus Primer - Understanding the bus
====================================
D-Bus is first and foremost an Inter Process Communication (IPC) solution. But
it does a bit more than just passing a blob of information from one process to
another one. D-Bus is object oriented, where each object can have a set of
Methods, Properties and Signals. All the data passed through D-Bus is also
required to use the data types provided by D-Bus. This enforces a fairly
strict and predictable type checking, which means the application implementing
D-Bus will not need to care about parsing message protocols.
The D-Bus design is built around a server/client approach, where the server
side is most commonly referred to as a D-Bus service. And the client side
uses, what is often called, a proxy to do operations on D-Bus objects provided
by a specific D-Bus service. Operations can be calling methods or reading or
writing object properties. D-Bus object are referenced through D-Bus
paths. And a D-Bus service is accessed through a destination reference, which
is a bus name and a bus type indicator.
Bus types
---------
D-Bus also operates with several types of buses. The most commonly used
implementations use either the _system bus_ or the _session bus_. The system
bus is by default fairly strictly controlled and locked down. Users can only
access services on the system bus if the policy for that service allows
it. The session bus is only reachable by a single user, and most commonly also
only in a specific sessions. The session bus is also not that strict
configured, as its availability is much more reduced. For example if you are
logged in through a graphical desktop login, you most likely have a session
bus running for that login. If you at the same time from another computer log
in via SSH to the same user account on the same graphical desktop host, that
SSH login will not have access to the session bus which the graphical desktop
login uses - despite the username and host being identical.
Bus names
---------
Each process implementing a D-Bus is required to have a unique bus name,
regardless if it has the role of a server or client. This is typically using
the `:X.Y` notation. Also beware that this unique bus name is provided by the
D-Bus messaging daemon which is most likely running on your system
already. That means it is not a consistent value. In addition a service can
own a more generic and human readable service name, most commonly called a
well-known bus name. Such a well-known name can for example be
`net.openvpn.v3.configuration`. This is used for the server side
implementations, to have a predictable destination reference from the client
side. The implementation of the service instructs the D-Bus message daemon
what should happen if there is conflicting well-known bus names; if new
services should be denied, if existing services should loose the ownership to
the well-known bus name or if exiting services should just ignore it and
continue to run only to be reachable via the unique bus name. But it is also
important to beware that many of the responses received may very well return
the unique bus name and not the well-known bus name you used when connecting
to a D-Bus service.
Getting unto the bus
--------------------
To reach a D-Bus object, you need to first connect to either the system or
session bus. Then you need to get in touch with the service where it is most
common to use the well-known bus name. A well-known bus name is basically a
human readable string name, organized as a reversed domain name. For the
OpenVPN 3 Linux client we will use the base domain of `net.openvpn.v3`. When
connecting to a specific service, the well-known name is referred to as the
_destination_. Within that service you will have one or more objects
available. Using the proper object path, you often have a selection of
interfaces, where each interface have their own set of Methods, Signals and
Properties.
D-Bus Methods are functions you can call, which will be executed by the D-Bus
service. D-Bus properties are variables owned by an object which you can read
or write to, depending on their attributes. And D-Bus signals are events
happening inside a D-Bus service which it can use to get some attention to
changes. To receive such signals, you need to subscribe to them first.
Lets have a look at a simpler D-Bus service. We will here look at the service
used to start OpenVPN 3 VPN backend client processes. We will not look into
the service itself now, but focus on how to talk to this service.
```
$ gdbus introspect --system --dest net.openvpn.v3.backends --object-path /net/openvpn/v3/backends
node /net/openvpn/v3/backends {
interface org.freedesktop.DBus.Properties {
methods:
Get(in s interface_name,
in s property_name,
out v value);
GetAll(in s interface_name,
out a{sv} properties);
Set(in s interface_name,
in s property_name,
in v value);
signals:
PropertiesChanged(s interface_name,
a{sv} changed_properties,
as invalidated_properties);
properties:
};
interface org.freedesktop.DBus.Introspectable {
methods:
Introspect(out s xml_data);
signals:
properties:
};
interface org.freedesktop.DBus.Peer {
methods:
Ping();
GetMachineId(out s machine_uuid);
signals:
properties:
};
interface net.openvpn.v3.backends {
methods:
StartClient(in s token,
out u pid);
signals:
Log(u group,
u level,
s message);
properties:
};
};
$
```
The command line used here, uses the D-Bus introspection feature which most
services provides. It gives an idea of what is available and the required
API. We ask for the introspection data of a service on the system bus (The
`--system` argument). Further, we connect to the `net.openvpn.v3.backend`
service and ask for the introspection data of the object located under
`/net/openvpn/v3/backends`.
The result provides a fairly comprehensive list which includes four different interfaces:
* `org.freedesktop.DBus.Properties`
* `org.freedesktop.DBus.Introspectable`
* `org.freedesktop.DBus.Peer`
* `net.openvpn.v3.backends`
In this section, we will only look at the last interface, `net.openvpn.v3.backends`.
This interface declares one method (`StartClient`) and a one signal
(`Log`). The method declares that it takes one input argument (`in s token`)
and provides one output argument (`out u pid`). Also note that D-Bus methods
may be declared to return several variables at the same time. And when the
proxy code parses the response, it may use the variable names as indicated.
The '`s`' reference in the variable declaration indicates that the data type
is a 'string'. The return variable is named pid and is an unsigned integer. So
to call the `StartClient` method, it is needed to provide a single string
containing a token value and on success it will return with a 'pid' variable
containing an unsigned integer. You can read more about the various data types
D-Bus supports here:
[https://dbus.freedesktop.org/doc/dbus-specification.html#type-system](https://dbus.freedesktop.org/doc/dbus-specification.html#type-system)
The `Log` signal is pretty much similar. The difference is that you don't call
signals, but subscribe to them. Whenever this D-Bus service issues the `Log`
signal, you will be provided with three variables in your signal handler code:
Two unsigned integers (`group` and `level`) as well as a string
(`message`). You will not get any heads up that a signal is being sent,
neither can you tell the D-Bus service to "send me all the unprocessed
signals". They happen asynchronously, and if you don't pay attention to it,
you will have lost the signal. So it can be fragile if not implemented
correctly.
Interacting with a D-Bus service
--------------------------------
Lets take a more advanced example, where we import a simple configuration file
to the OpenVPN 3 D-Bus Configuration Manager. First we will do an
introspection to see the API being provided.
```
$ gdbus introspect --system --dest net.openvpn.v3.configuration --object-path /net/openvpn/v3/configuration
node /net/openvpn/v3/configuration {
/\* Removed the org.freedesktop.DBus.* interfaces for clarity */
interface net.openvpn.v3.configuration {
methods:
Import(in s name,
in s config_str,
in b single_use,
in b persistent,
out o config_path);
signals:
Log(u group,
u level,
s message);
properties:
};
};
$
```
The configuration manager have an `Import` method which we will need to
use. Lets write a simple Python script to interact with this service and
import a simple configuration file. The important detail is that the `Import`
method takes four arguments, two strings and two boolean values and it will
return an object path - which is essentially a string with quite strict checks
on its content. See the
[net.openvpn.v3.configuration](dbus-api-net.openvpn.v3.configuration.md)
D-Bus API specification for details on these variables.
```python
import dbus
# Get a connection to the system bus
bus = dbus.SystemBus()
# Retrieve the main configuration manager object.
# We provide the service name and the initial object path as arguments.
# Those need to correspond to what we saw in the introspection.
manager_object = bus.get_object('net.openvpn.v3.configuration', # Well-known bus name
'/net/openvpn/v3/configuration') # Object path
# Retrieve access to the proper interface in the object
config_interface = dbus.Interface(manager_object,
dbus_interface='net.openvpn.v3.configuration')
# Here is our super simple configuration file (not valid by the way, as it lacks --ca)
config_to_import = """
remote vpnserver.example.org
port 30001
proto udp
client-cert-not-required
auth-user-pass
"""
# Import the configuration, and we're given back an object path
config_path = config_interface.Import("Test config", # name
config_to_import, # config_str
False, # single_use
False) # persistent
print("Configuration path: " + config_path)
```
When running this little Python script, the result will be something like
this:
```
Configuration path: /net/openvpn/v3/configuration/2e356d14x6d51x4d16xbaf3x3d758626fc82
```
This is essentially a new D-Bus object managed by the
`net.openvpn.v3.configuration` service. When the process providing this object
stops, this object is also not available any more. And to access this new
object, we need to use the path returned by the `Import` method. So lets
introspect that object as well.
```
$ gdbus introspect --system --dest net.openvpn.v3.configuration --object-path /net/openvpn/v3/configuration/2e356d14x6d51x4d16xbaf3x3d758626fc82
node /net/openvpn/v3/configuration/2e356d14x6d51x4d16xbaf3x3d758626fc82 {
/\* Removed the org.freedesktop.DBus.* interfaces for clarity */
interface net.openvpn.v3.configuration {
methods:
Fetch(out s config);
FetchJSON(out s config_json);
SetOption(in s option,
in s value);
Seal();
Remove();
signals:
properties:
readonly s name = 'Test config';
readonly b valid = true;
readonly b readonly = false;
readonly b single_use = false;
readonly b persistent = false;
};
};
$
```
And we can recognise several of these properties, as they were provided during
the `Import` method call. But we also have a different set of methods
available too. We can now use a simple `dbus-send` command line to call thee
`FetchJSON` method. Notice that when calling methods from the command line, we
need to use the interface name as well as the method name.
```
$ dbus-send --system --print-reply=literal --dest=net.openvpn.v3.configuration \
/net/openvpn/v3/configuration/2e356d14x6d51x4d16xbaf3x3d758626fc82 \
net.openvpn.v3.configuration.FetchJSON
{
"auth-user-pass" : "",
"client-cert-not-required" : "",
"port" : "30001",
"proto" : "udp",
"remote" : "devtest1.openvpn.in"
}
$
```
It is also possible to use other D-Bus clients as well, to achieve the same
goal. But the output being presented can often be different. So for reference,
here is how to do the same call with both gdbus and qdbus (from Qt)
```
$ gdbus call --system --dest net.openvpn.v3.configuration \
--object-path /net/openvpn/v3/configuration/2e356d14x6d51x4d16xbaf3x3d758626fc82 \
--method net.openvpn.v3.configuration.FetchJSON
('{\\n\\t"auth-user-pass" : "",\\n\\t"client-cert-not-required" : "",\\n\\t"port" : "30001",\\n\\t"proto" : "udp",\\n\\t"remote" : "devtest1.openvpn.in"\\n}',)
$ qdbus --system net.openvpn.v3.configuration \
/net/openvpn/v3/configuration/23165540x6646x42bexb530x464bb8e2df67 \
net.openvpn.v3.configuration.FetchJSON
{
"auth-user-pass" : "",
"client-cert-not-required" : "",
"port" : "30001",
"proto" : "udp",
"remote" : "devtest1.openvpn.in"
}
$
```
### Accessing object properties
Accessing properties in D-Bus objects is a bit more tricky. Here it will be
demonstrated how to reach the `name` property in the configuration object we
have already available.
It is needed to use a specific interface within D-Bus framework to access
properties. These methods are located inside the
`org.freedesktop.DBus.Properties` interface in each available object. First,
lets look closer at that interface, again by using the introspection
possibility.
```
$ gdbus introspect --system --dest net.openvpn.v3.configuration --object-path /net/openvpn/v3/configuration/2e356d14x6d51x4d16xbaf3x3d758626fc82
node /net/openvpn/v3/configuration/2e356d14x6d51x4d16xbaf3x3d758626fc82 {
interface org.freedesktop.DBus.Properties {
methods:
Get(in s interface_name,
in s property_name,
out v value);
GetAll(in s interface_name,
out a{sv} properties);
Set(in s interface_name,
in s property_name,
in v value);
signals:
PropertiesChanged(s interface_name,
a{sv} changed_properties,
as invalidated_properties);
properties:
};
/\* Removed the rest, for clarity */
};
```
The method we are interested in is the `Get` method in the
`org.freedesktop.DBus.Properties` interface. This method requires two string
arguments, the first one is the _interface_ carrying the property we want to
read. The second argument is the _name_ _of the property_ we want to access.
```
$ dbus-send --system --print-reply --dest=net.openvpn.v3.configuration \
/net/openvpn/v3/configuration/2e356d14x6d51x4d16xbaf3x3d758626fc82 \
org.freedesktop.DBus.Properties.Get \
string:'net.openvpn.v3.configuration' string:'name'
method return sender=:1.2920 -> dest=:1.3560 reply_serial=2
variant string "Test config"
$
```
The arguments we send to the `Get` method needs to be typed, so `dbus-send`
have its own syntax to handle that from the command line. Other programming
languages have their own ways of handling types. Python will for example
mostly do that automatically for you.
And just for reference, using `gdbus` and `qdbus` to do achieve the same. As
you can see by these examples, the arguments are considered to be strings by
default using these tools.
```
$ gdbus call --system --dest net.openvpn.v3.configuration \
--object-path /net/openvpn/v3/configuration/2e356d14x6d51x4d16xbaf3x3d758626fc82 \
--method org.freedesktop.DBus.Properties.Get net.openvpn.v3.configuration name
(<'Test config'>,)
$ qdbus --system net.openvpn.v3.configuration \
/net/openvpn/v3/configuration/23165540x6646x42bexb530x464bb8e2df67 \
org.freedesktop.DBus.Properties.Get "net.openvpn.v3.configuration" name
Test config
$
```
Conclusion
----------
To access a D-Bus object, you need to:
* Connect to the proper bus (system or session)
* Know which destination service you are targeting
* Know which object you want to work with
* Know which interface within that particular object you want to access
With these four areas covered, you have direct access to all the available
methods and properties inside any object on the D-Bus.
### Challenges
1. Implement retrieving an object property using Python or another language
with D-Bus bindings available
2. Try changing the `name` property of the configuration profile object.
* What happens? Try to do introspection on both the main configuration
manager object and this specific configuration profile object.
|