Skip to content

The Network Configuration Daemon, is a daemon and programming language for configuration of network interfaces and other aspects of the operating system made by Ambrož Bizjak.

License

Cloudxtreme/NCD

 
 

Repository files navigation

  1. summary NCD - powerful and extensible network interface configuration
  2. labels linux,network,interface,configuration,hotplug,dhcp,flexible,powerful

Table of Contents

Contents

<wiki:toc max_depth="1"></wiki:toc>

Introduction

NCD, the Network Configuration Daemon, is a daemon and programming language for configuration of network interfaces and other aspects of the operating system. It implements various functionalities as built-in modules, which may be used from an NCD program wherever and for whatever purpose the user needs them. This modularity makes NCD extremely flexible and extensible. It does a very good job with hotplugging in various forms, like USB network interfaces and link detection for wired devices. New features can be added by implementing statements as C-language modules using a straightforward interface.

NCD is only available for Linux. It is included in the BadVPN software package.

Why ?

If you use Linux on a desktop system, you must have heard of a program called !NetworkManager. It is a program that is meant to take care of network configuration on the OS. It is designed to be extremely easy and simple to use; it has a GUI with a wireless network list, easy wireless and IP configuration, support for VPN, etc..

This all seems good. Until you want to use two VPNs at the same time. Or add some nontrivial iptables rules. Or tell it not to mess with the network interface called eth1. Or setup a bridge interface. Basically, it's a hardcoded mess, and if you want it to do something the designers haven't specifically foreseen and implemented, better go crying right away.

Other network configuration frameworks have similar problems. The Gentoo init scripts, for example, can't even do DHCP right (it times out and forgets about the interface forever)!

My conclusion was that, for more powerful and extensible network configuration, the existing systems need to be thrown away; a new system better suited to the needs of power users needs to be designed. So here comes NCD!

Basics

NCD is used by writing an NCD program (the "configuration file"), then starting the `badvpn-ncd` program as a daemon (usually at system boot time). NCD will then keep running the supplied program, until it is requested to terminate (usually at shutdown time). NCD is designed to work in the background without any need to communicate with the user, and to automatically recover from any transient problems. The goal is to make things (e.g. network access) "just work", while still allowing dynamic and complex configuration.

Model of execution

An NCD program (see example below) consists of a set of so-called *processes*. Globally, the processes can be considered to execute in parallel. Each process consists of a list of *statements*.

When NCD starts up, all statements are *uninitialized*. Then the first statement in every process is *inizialized*. An initialized statement is first in the *down* state. It may at any time transition into the *up* state. When it does so, the next statement in the process is initialized, and when this one transitions up, the next one is initialized, etc..

A statement (A) that is initialized and up can, at any time, go back into the down state. When this happens, NCD will cease initializing statements in the containing process, and will begin *deinitializing* statements, from the *bottom up*, until all statements following statement A are uninitialized; it will then wait for the offending statement A to go back up, and continue initializing statements from that point.

The deinitialization of a statement often has the *reverse effect* of initialization; for example, if a statement added a routing table entry on initialization, it will remove this same routing table entry on deinitialization. This design is very suitable for various configuration tasks, as it avoids possible leaks. This design makes it trivially easy to, for example, make sure than when a network cable is pulled out, the associated routes and IP addresses are removed.

When a statement is initialized, it is provided with a list of *arguments* which tell the underlying module specifically what it is supposed to do. An argument is either a string or a list of strings and lists. Arguments can either be provided literally in the statement specification, or they may refer to *variables*, whose values are provided by preceding statements in the process. Variables are key to the power of NCD.

A statement is specified as `statement(arg1, ..., argN);`. Optionally, it can be given an identifier: `statement(arg1, ..., argN) identifier;`.

A variable is used by passing `object.variable` as an argument. Here, `object` is an identifier of one of the statements in the process, declared somewhere above (later statements with that name shadow the earlier); `variable` is the name of the variable within this statement. Additionally, if only `object` is passed, it refers to the empty string variable within `object` (thus, you're still passing a *value* (string, list), *not* a reference to the statement).

When NCD is requested to terminate, it starts deinitializing all processes from the bottom up, like described above, and this deinitialization continues until all statements in all processes are deinitialized. NCD then exits.

The NCD interpreter by itself does not provide any control constructs. These are however implemented as statements which communicate behind the back of the interpreter. An example of this are the `provide()` and `depend()` statements.

Complete example

This is an example NCD program that works with a single wired network interface and uses DHCP to obtain the IP address, default route and DNS servers.

The above code, when read from top to bottom, simply specifies which operations are to be performed in order to reach the desired state. However note that each NCD program has an implicit deinitialization semantic. Here, for instance, pulling the network cable out will **automatically** remove the DNS servers, remove the default route and remove the IP address from the network interface.

More examples

See [NCD_examples] for more complex examples. NCD is capable of much more than the above program may suggest. For example, it can handle multiple network interfaces with priorities for Internet access, it can work with wireless networks and BadVPN network interfaces.

There is also an alternative introduction to NCD, and more examples, which include BadVPN interfaces: http://code.google.com/p/badvpn/source/browse/trunk/ncd/README .

Requirements

NCD requires various programs during execution. In particular:

  * *iproute2* (`ip` command) is needed by the `net.up`, `net.ipv4.addr` and `net.ipv4.route` modules. Not all distributions come with that; Gentoo for example doesn't.

  * *udev >=171* is needed for `net.backend.waitdevice` and `net.watch_interfaces`.

Running it

For installation instructions see [Installation].

Disabling existing network configuration

Before you start NCD, you have to stop any existing network configuration system to avoid interference.

  * Gentoo: stop `NetworkManager` init script, stop `net.` init scripts, except `net.lo`.
  * Ubuntu: stop !NetworkManager using `initctl stop network-manager`

Also, !NetworkManager has a habit of not deconfiguring interfaces when stopped. If you had !NetworkManager running:

  * Kill dhclient: `killall dhclient`
  * Remove IP addresses: `ip addr del <addr>/<prefix> dev <iface>`
  * Set down: `ip link set <iface> down`

Testing from command line

Once you're sure your interfaces are deconfigured and there is nothing that could interfere, then try the program (`/etc/ncd.conf`) out by running (as root):

NCD will print status messages as it executes your program. If it's not working as expected, these can help you with debugging it.

Automatically

If you installed BadVPN via a package manager, NCD is integrated into your distro's init system, to use `/etc/ncd.conf` as the NCD program. To use NCD by default, you will have to permanently disable existing network configurations, and have NCD start on boot instead.

To disable existing network configurations:

  * Gentoo: Disable `NetworkManager` init script. Delete `net.` init scripts (which are symlinks to `net.lo`), except `net.lo` itself, to prevent Gentoo from autoconfiguring interfaces.
  * Ubuntu: Disable !NetworkManager by editing `/etc/init/network-manager.conf`, commenting the two `start on` lines, or by uninstalling the `network-manager` package.

To have NCD start on boot:

  * Gentoo: Enable the `/etc/init.d/badvpn-ncd` init script: `rc-update add badvpn-ncd default`
  * Arch: Enable the `/etc/rc.d/badvpn-ncd` init script by adding it to `DAEMONS` in `/etc/rc.conf`. Make sure it comes after syslog.
  * Ubuntu: Use the `/etc/init/badvpn-ncd.conf` Upstart script. The script is disabled by default; the `start on` line inside it is commented out. Uncomment it to have it start on boot. However be aware that `apt-get` will start it regardless when it installs the `badvpn` package (the default `/etc/ncd.conf` is a no-op to avoid damage here). You can also manually control the service using `initctl <start/stop/restart/status> badvpn-ncd`. 

Module documentation

Individual statement types (modules) are described briefly in the headers of their source files, under ncd/modules/ in the BadVPN source code: http://code.google.com/p/badvpn/source/browse/#svn%2Ftrunk%2Fncd%2Fmodules .

NCD lanaguage introduction

Here's a quick introduction to the programming language of NCD.

Diagnostic output

The println() and rprintln() statements provide diagnostic output to standard outout. `println()` prints a message on initialization, and `rprintln()` prints a message on deinitialization.

This should result in something like this:

Dependencies

The provide() and depend() statements implement dependencies.

Suppose we want to wait for a network device, and have some kind of service that works with it:

However, what if, after the device is available, we want to run _two services in parallel_?

Note how the service processes access the `dev` variable within process `foo` through the dependency. A `depend()` allows access to any variable as seen from the point of the matched `provide()`.

Because of how the NCD interpreter works, `provide()` should usually be the *last statement* in a process. Otherwise, when something before `provide()` goes down, a latter statement could take some time to deinitialize, and during this time, the depending processes may continue initalizing and may request variables through their `depend()`-s - which will fail, because `provide()` is scheduled for deinitialization and cant't resolve variables.

If you wanted to have some statements after a `provide()`, you should instead make a new process that `depend()`-s on this same `provide()` and put the statements there.

There are however exceptions to this rule. For example, the following code wakes up two parallel processes, but then merges back. It's safe because when process `main` is requested to terminate after it merged back, the two parallel processes have initialized completely and won't resolve any variables.

Branching (simple)

NCD does not provide any nested control structures, such as `if` and `for`, as found in many languages; instead, execution control is done with special-purpose statements.

A simple way to branch is using provide() and depend() statements.

However, this is an *inferior* way to branch, because the dependency names (`branch` and `branch_done`) have global scope. This means that each branch needs its unique set of dependency names, so as not to conflict with unrelated `provide()` and `depend()` statements.

A proper way to branch is using the `call()` statement (read on).

Process templates

Many of NCD's execution control features rely on a feature called *process templates*. A process template is written like a process, but using the `template` keyword instead of `process`. Unlike a regular process, which starts up automatically and is terminated automatically on NCD shutdown, a process template by itself does nothing. Instead, special statements are used to dynamically create *template processes* out of process templates.

A template process, however, is an *actual process*; its code is that of the process template is was created from. Unlike regular processes, template processes do not terminate automatically when NCD is requested to shut down; termination of a template process needs to be requested by its controlling code (often by the same statement that created it).

Because all template processes created out of the same process template have the same code, there needs to be a way to distinguish them. All statements that create template processes take a list of arguments, which is accessible from resulting template processes via `_argN` special variables. Additionally, the code creating a template process can provide other special variables.

Some statements that create template processes are `call()`, `foreach()` and `process_manager()`. Read on to learn about them.

Calling templates

The effect of call() is mostly equivalent to embedding the body of the called process template into the place of `call()`, including correct reverse execution.

Additionally, with `call()`, you can:

  * Pass arguments to the template process. These can be accessed from the template process via the `_argN` special variables (N-th argument, starting with zero), and the `_args` special variable (list of arguments).
  * Access caller's objects from within the called template process via `_caller`.
  * Once the called template process has initialized, access its objects through the name of the `call()` statement.

The following example demonstrates these features.

Note that it is impossible to define new statements from within the NCD programming language. The only way to define a new statement is to extend NCD by implementing the statement in C language using NCD's module interface. However, when considering a new statement, you should always first try to implement the same functionality in the NCD programming language, which would usually involve using process templates in some way.

You can compare the NCD interpreter and its programming language to a CPU and its machine language. Similarly to how you can't add new machine instructions to the CPU without changing its hardware (or microcode), you can't add new statements to NCD without changing its source code. On the other hand, like you _can_ write subroutines and call them using machine instructions, you can write process templates for NCD and call them using the `call()` statement.

Branching (correct)

The `call()` statement can (and should) be used for branching. This works by building the name of the called template dynamically based on runtime values. The following program demonstrates this.

Compared to simple branching using `provide()` and `depend()`, this branching mechanism is safe to use from template processes.

Multi-way branching

The choose() statement together with `call()` can provide "if, else if, ..., else" style branching.

One-way branching

It is possible to do a one-way branch by giving `call()` `"<none></none>"` as the template name to make it do nothing, possibly using `choose()`:

Foreach

foreach() does something for each element of a list. It is mostly equivalent to putting multiple `call()` statements one after another, but it allows the elements to be specified dynamically in form of a list.

This results in the following:

The template process created by `foreach()` for every element of the list can access its element using `_elem`. Additionally, like `call()`, it can access arguments passed to `foreach()`, and any variable or object `X` as seen from the point of `foreach()` via `_caller.X`.

The template processes are managed by `foreach()` in the same way as the NCD interpreter initializes and deinitializes statements within a process. In particular, if a statement in one of the template processes goes down, `foreach()` will pause this process, deinitialize any following processes from the bottom up, and only then continue the process where a statement went down (assuming no prior statements went down).

This means that you can use `foreach()` to acquire a set of resources at once. For example, to wait for to all network interfaces on a list to appear:

Method-like statements

Note how the arguments of statements in NCD are restricted to plain data, and there is no concept of a reference to an object. Sometimes, however, it is needed for two or more statements to cooperate in their implementation, and you might need to tell one statement which existing related statement to cooperate with.

NCD solves this with *method-like statements*. These statements are just like regular statements, except that they refer to some existing statement, and their implementation uses this information somehow. A method-like statement is written by preceding the method name with an object identifier and an arrow `->`. The following example demonstrates the list::contains() statement.

Note that the `list::contains()` statements here only use the referred list object when they initialize, and not after that. In general, however, method-like statements can cooperate with the referred object however long they like; they can also go up and down like regular statements can.

The restriction on arguments being plain data applies not only to statement arguments, but on template process arguments too. For example, the following code is *incorrect*:

In this case, the error can be corrected by having the template process construct its own list object:

On the other hand, if it's truly necessary to invoke a method-like statement on an object of the caller, this can be done by going through `_caller`:

It's easy to see that how the above approach is clumsy to work with, because the template expects the object to have a predefined name. This can be fixed using the `alias()` statement, described next.

Aliases

The alias() statement allows a group of variables and objects to be referred to using a new name.

Notice how the target of the alias is given as a string. When the `alias()` statement initializes, the target is not resolved in any way. In fact, all that `alias()` does is forward variable and object resolution requests by prepending the target string, plus possibly a dot, to the requested name, and resolving it from its point of view.

An important use of `alias()` is to simulate passing actual objects through a `call()` (compared to passing just data), such that the called process can invoke the object's methods. Here's the last example from the description of `call()`, fixed to allow specifying the object name:

Multi-dependencies

The multiprovide() and multidepend() statements implement dependencies similar to `provide()` and `depend()`. Contrary to `provide()`, which will shout errors if another `provide()` for the same name is already active, it is possible to have multiple parallel `multiprovide()`s satisfying a single `multidepend()`. On the other side, a `multidepend()` will always attempt to bind itself to one of the `multiprovide()`s that satisfy it. It will always choose the best one based on its preference list, possibly un-binding itself from an existing `multiprovide()`.

The following example illustrates the behavior of `multiprovide()` and `multidepend()`:

This will result in the following:

As can be seen, unlike `depend()`, which only goes down when its bound `provide()` is broken, `multidepend()` also goes down when a better `multiprovide()` comes in.

Note that the namespaces for dependency names of `provide()`/`depend()` and of `multiprovide()`/`multidepend()` are separate.

Event-reporting modules

Many statements in NCD can be considered to report events. However, until now, events were reported only in form of a module going up or down. Some examples of this are `net.backend.waitdevice()`, `net.ipv4.dhcp()` and `depend()`. Sometimes, however, events may need to be treated as just events, without a natural correspondence to the up/down state of a statement.

General event reporting facilities in NCD are implemented in form of a statement which behaves as follows:

  # When an event occurs, the statement goes up and exposes the information about the event via its variables.
  # The statement has a `::nextevent()` method that is called to indicate that the current event has been handled; this makes the event reporting statement go back down, waiting for the next event.

The following example demonstrates the sys_evdev() statement, which reports events from a Linux event device (evdev).

To run this, you have to provide an existing `-event-` (!) device, and you need read permission on the device. If all goes well, this will result in NCD spamming the console with something in form of the following (the values here correspond to what is defined in the `linux/input.h` header file):

Note how the above example executes in a conceptually different way to what has been demonstrated before. In previous examples, processes were meant to advance forward towards a goal, and only revert to a previous state when something goes wrong. In this case, however, the process reverts as soon as `nextevent()` is reached, deinitializing all statements between `nextevent()` and `sys.evdev()`. While it may not appear as such at first look, it is really a *loop*. (feel free to indent the statements following `sys.evdev()` if you like it better that way)

Process manager

Event-reporting modules are not of much use without a useful way to handle events. The process_manager() module provides the ability to spawn new processes in response to events. The following example demonstrates the behavior of process_manager():

This will result in this:

See how start() creates a new process and gives control to it; however, as soon as the created process cannot continue (in this case, entering sleep()), control is returned to the process that called start(). In fact, start() is done after it has spawned the process - it will not do anything from that point on, and will *not* stop the process when deinitializing. Instead, process_manager() will stop its processes when deinitializing, waiting for them to terminate.

Processes created via process_manager() can also be stopped explicitly using the stop() method, by providing the same process identifier as in the corresponding start() call:

This will produce the following:

In this case, start() spawns the process, which proceeds to sleep(), at which point control is returned to the `foo` process. This one then calls stop(), triggering the deinitialization of the process that was just spawned, requesting its sleep() statement to terminate. sleep() again returns control to `foo`, which prints "Foo done.". After 3 seconds, sleep() finally deinitializes.

Handling events with process manager

process_manager() can be used in combination with event reporting modules, in particular with those that report the presence of hardware devices, to automatically create processes that configure them:

This results in automatic starting and stopping of network interface configuration processes based which network interfaces exist in the system at any given time. Be aware that an `interface_worker` will be created for *all* network interfaces, including the loopback, wired and wireless interfaces. To configure them properly, branching must be used.

About

The Network Configuration Daemon, is a daemon and programming language for configuration of network interfaces and other aspects of the operating system made by Ambrož Bizjak.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • C 96.1%
  • PHP 1.4%
  • Roff 0.7%
  • Yacc 0.6%
  • CMake 0.5%
  • C++ 0.5%
  • Other 0.2%