*******************************************
*IPCFG: A modern network management system*
*******************************************

1. Overview.
============

Ipcfg is a highly flexible replacement for the traditional "ifupdown"
suite of programs. Based on an internal state machine, and with a plugin
interface, it provides users with a flexible configuration system,
suitable for laptops and servers alike, while having a carefully
designed and extendable API, allowing programmers to easily extend its
functionality.

The internal system is a state machine based on function pointers. There
are three important concepts: "confignodes", "events", and the "context"
structure.

The first of these concepts, the 'confignode', is the state machine.
When a user runs 'ifup home', what happens internally is that ipcfg will
look in a hash table for a confignode with the 'home' name (which may or
may not refer to the 'home' physical interface), and execute the
function pointer that is part of this confignode. If the return value of
this function indicates success (that is, it is zero), and the 'success'
pointer is non-NULL, then the same procedure is repeated for the
confignode behind this success pointer. If the return value is nonzero,
and the 'failure' pointer non-NULL, then this confignode is executed
instead. This procedure is repeated until the system encounters a NULL
pointer for either the 'success' or 'failure' pointers, in which case
the result of the final function is returned all the way up the stack.

The second concept provides a generic event-based system. Any plugin may
define any events; any plugin may register callback handlers for any
events, either those defined by itself, or those defined by other
plugins or even the core system. When the event triggers, the system
will call the function pointer. The result value of the 'signal_event'
function (which executes the callback handlers) is the sum of all the
return values of the functions that have been registered and executed.

The third of these concepts, the "context", allows a plugin developer to
track the actions of the state machine. For instance, when the user
performs an 'ifup auto' (which will bring up any network that was must
be configured at boot time), the context will not be the same as when
the user asks for 'ifup eth0', even if both actions produce the same
result. Likewise, when a confignode wants to perform a number of tests
on a network without specifying which network interface we're using, the
system will record what interface we're working on in the context; and
the results of these tests will also be stored in the context.

The system has a number of global indexes: the "test" index, the
"config" index, the "action" index, and the index of named confignodes.

Any plugin may register a "test", which requires a function pointer that
points to a function suitable for the 'fptr' pointer in the confignode
system. These test functions may expect arguments; if they exist, they
will be available as a DLList* in the "data" argument of the confignode.
Registering a test must be done in the initialization function of that
plugin, so that it is immediately available. Tests may set variables in
the context, and have to bring a link physically up (so that a wireless
interface can scan for Access Points, or so that a wired interface can
test for a cable, or some such), but should ideally have no other
side-effects.

The implementation of the "action" index is suspiciously similar to the
implementation of the "test" index. They are separate, because they
perform different functions in the config file; however, from an API
point of view, they are the same. An action should ideally not modify
context variables, and should have exactly one significant side-effect.
For instance, it could set an IP address on an interface, or set the
default route, or something similar. Actions can take arguments as well;
they too are specified as a DLList* on the 'data' pointer of the
confignode.

Any plugin may register an anonymous confignode in the "config" index.
This allows these confignodes to be referred to from live
configurations. Note that the user may also register any type of
configuration in the "config" index. An item in the "config" index could
be thought of as a compound action, or a compound test -- or a
combination of both. One cannot specify arguments to a confignode, but
it is of course possible to specify values through the context.

The index of named confignodes is special, in that it does not pay
attention to namespaces. For this reason, plugins should not create new
named confignodes.

2. API Details
==============

2.1. Confignodes
----------------

From the source:

struct _cnode {
	char* name;		/**< The name of this confignode, which is
				 * the logical name of this
				 * configuration. May or may not contain
				 * the same value as ifname. */
	char* ifname;		/**< The name of the interface we're
				 * trying to modify */
	void* data;		/**< Data for the function pointer. */
	int(*fptr)(struct _cnode*, ipcfg_action, ipcfg_context*);
				/* Function that will try to do
				 * something. */
	struct _cnode* success; /* If fptr() returns zero and this
				 * pointer is non-NULL, this confignode
				 * is performed. If it is NULL,
				 * perform_confignode returns and the
				 * configuration which we've found is
				 * activated */
	struct _cnode* failure; /* If fptr() returns nonzero and this
				 * pointer is non-NULL, this confignode
				 * is performed. If this pointer is NULL
				 * and fptr() failed, then the ifup or
				 * ifdown action is assumed to fail. */
};

typedef struct _cnode ipcfg_cnode;

Fairly basic. Also see the above.

The "fptr" function will receive a pointer to _this_ confignode. This
allows it not only to find the "data" pointer (which the confignode
framework does not care about), but also to investigate the state
machine. Plugin developers should however refrain from doing weird stuff
with confignodes after everything has been parsed, for obvious reasons.

The difference between the "name" and "ifname" strings is that the first
is a _symbolic_ name; when the user asks to bring up "eth0", the system
will look for a confignode with "eth0" in the "name" string. This
logically means that there can only be one confignode in the system with
a given name. On the other hand, the "ifname" contains the name of the
interface that this confignode is supposed to work on; there can be many
confignodes with the same ifname. The "name" and "ifname" strings need
not be the same; this allows to create one configuration "home", and one
"work", which both use the "wlan0" interface. Or the "work"
confignode might not contain an interface name at all, with the actual
interface defined through the context. This is left up to the developer.

To create a new confignode, use the "get_confignode_for()" or
"get_anonymous_confignode()" calls, depending on whether you need a
named or anonymous confignode. While it is possible to fill in the name
of an anonymous confignode, this will *not* result in the same effect as
what would be received when creating a non-anonymous confignode in the
first place. It also is not possible to turn an anonymous confignode
into a named one. This is on purpose. If you want to change the system
at runtime rather than at configuration time, use the event system.

It is of course possible to overwrite a named confignode with another
confignode. This will, however, result in broken behaviour, and should
not be done. Instead, developers should follow the confignode tree down
the 'success' line until they find a NULL pointer, and append values
there. Usually, however, it is not necessary to manually walk the tree
-- the config file parser will do this for you.

To run a confignode, use the "perform_confignode" function. There is
also a "perform_confignode_no_fail" function, which would ignore the
"failure" pointer of the topmost confignode (but not those of any
confignodes below the topmost one).

2.2. Events
-----------

Events are known from other frameworks (e.g., the 'signals' in GNOME's
GObject system). Events in ipcfg have three properties: a "name", an
"event", and an "action". The name refers to the "name" of the object
(confignode, interface, ...) that generated the event. The "event" is
the name of the event; this is a free-form string. The "action" explains
whether we were trying to bring an interface up or down.

This system allows for the creating of "wildcard" event handlers. For
instance, it is possible to register an event handler for "failure to
bring up _any_ interface", or "anything related to eth0". It is also
possible to register a fully-wildcard event handler, which could be
useful for debugging and/or logging.

To register an event handler, use the "register_event_handler" function.
To trigger an event, use the "signal_event" function. Note that this
function takes a context as an argument; it is not an error to provide a
NULL pointer if no useful context exists.

The core system currently provides the following events:
- node_success: a named confignode was performed successfully. The
  "name" part of the event will be the name of the confignode.
- node_failure: a named confignode was not performed successfully. The
  "name" part of the event will be the name of the confignode.
- iface_success: a confignode (named or anonymous) with a value in the
  "iface" pointer was performed successfully. The "name" part of the
  event will be the name of the interface. Note that this event may
  trigger several times when bringing up a configuration where several
  succeeding confignodes have the same name in the 'ifname' member.
- iface_failure: a confignode (named or anonymous) with a value in the
  "iface" pointer was not performed successfully. The "name" part of the
  event will be the name of the interface; this, too, may trigger
  several times.

2.3. Contexts
-------------

A "context" provides a plugin programmer with a means of detecting what
the user is trying to accomplish. For instance, the user might have
asked "give me a network -- /any/ network", or instead the user might
have asked "bring up interface eth0". A confignode might behave
differently -- for example, in error handling -- depending on which of
the two cases is true.

The context system has two types of members: members in a hash table,
and direct members. The direct members can be accessed much faster than
those in the hash table, and are only used for those context items that
are almost always useful to be in the context. At present, these are the
interface name we're working on, and the confignode that was the start
of our tree.

The others, as said, are in a data pointer that is part of the
ipcfg_context structure, and can only be accessed through five helper
functions: ipcfg_ctx_set_value(), ipcfg_ctx_unset_value(),
ipcfg_ctx_add_data(), ipcfg_ctx_unset_data(), and
ipcfg_ctx_lookup_data(). The first two of these can easily be used from
a confignode, and are primarily intended to be used by the config file
parser. As such, they will always set a value at the IPCFG_SRC_CONFIG
priority, and will only specify strings as data. The next two can be
used to set or unset any type of data, at any priority; however,
developers should not abuse priorities such as IPCFG_SRC_CMDLINE, for
obvious reasons.

Every piece of data in a context has a priority. The intention for this
is that the system should not try to override the interface name if the
user forced it to be something else in either configuration, or on the
command line, or something similar. This part of the system still needs
fine-tuning, however.

3. Plugins.
===========

A plugin is a shared object that is dlopen()ed by the main system.

Plugins are not loaded automatically; the plugins to be used are
specified in the configuration file, as an argument to the "plugins"
keyword. They live in whatever automake specifies as $(pkglibdir).

Plugins are loaded with RTLD_NOW | RTLD_GLOBAL | RTLD_DEEPBIND set in
the 'flags' field. This means that they can export symbols to other
modules, but that they will try to use their own symbols if possible.

After loading the module, ipcfg will do a lookup for a symbol called
"ipcfg_plugin_init", assign it to a function pointer, and jump to that
address. Therefore, every plugin should have a symbol with that name,
which can be assigned to a function pointer of type ipcfg_init_fn:

typedef int(*ipcfg_init_fn)(void);

If this function returns nonzero, it is assumed that loading it has
failed, and the module will be unloaded again. As such, modules that
return non-zero should take care to free any resources they may have
opened, such as memory or file descriptors, before returning; otherwise
the ipcfg binary may fail at any point in the future.

At no other point will a module be unloaded.

4. Backends.
============

The system has a strict separation of backend and frontend. The frontend
contains all the confignode and event handler logic; the backend will
perform actions on network interfaces.

This should make the system fairly portable between Linux, kFreeBSD, and
The Hurd (note that the author at this point has no interest in writing
backends for either FreeBSD or The Hurd, but patches are welcome).
Additionally, a "test" backend is created, which communicates with the
user over stdout and that should work on any POSIX system; this is
useful for debugging and/or development. It may or may not be complete,
but is selected with the '--with-backend=test' option to 'configure'.

For the time being, the used backend is selected at compile time. In the
future, the system may be extended so that backends can be selected at
run time.

Preferably, plugins should not implement backends, nor should they muck
with things that are supposed to be handled by the backend. Instead, in
the interest of portability, plugins should _use_ the backend. If
something is not provided by the backend, plugin developers should
provide patches and/or file wishlist items for the various backends to
implement the functionality that they would wish to see. Note, however,
that the above is a "should"; it is by no means forbidden to do
backend-like stuff from a plugin.

5. Config file keywords
=======================

The config file has two groups of keywords: 'global settings' keywords,
and 'configuration' keywords.

The former set a bunch of things related to what needs to be done at
system boot time etc; the latter allow one to configure in fine detail
what needs to happen when configuring an interface.

Note that the config file is not an interpreted or computed language;
rather, what is specified in a config file is transformed by the config
file parser to confignodes, as specified above.

5.1. General syntaxis.
----------------------

Everything that can be added by a plugin is quoted and namespaced.
Arguments and plugin-provided tests, actions, or configurations, are
specified as quoted strings:

namespace "firewall"

In this example, '"firewall"' is an argument to the "namespace" keyword;
the value of the argument is the string 'firewall', without the quotes.

The system has namespaces for everything except names of 'iface'
stanzas. There are two ways to specify a namespace: by using a
colon-separated string in which the first part is the namespace and all
other parts (including any colons after the first) are the name; or by
specifying the default namespace by way of the 'namespace' keyword, and
not using any colons in the name. This means that it is possible to have
a name as part of a namespace that contains colons, but it does mean you
have to fully specify the name, including its namespace, every time you
want to use it.

If no default namespace has been explicitly specified, the 'core'
namespace is assumed.

As such, the following two are equivalent:

config "firewall:work"

and

namespace "firewall"
config "work"

except that the latter changes the default namespace for everything that
comes after it.

As seen above, names with colons in them are legal, but then you must
specify the namespace every time:

config "perl:Some::Module"

would refer to the "Some::Module" configuration item in the hypothetical
"perl" namespace. Because this is confusing and error-prone, using
colons in names is discouraged, though not forbidden.

When an option takes a variable number of arguments, they need to be
specified as a list. A list looks like this:

("eth0", "eth1")

That is, a list needs to start with an opening bracket, contain one or
more comma-separated quoted strings, and end with a closing bracket.
When specifying just one argument, the brackets are optional.

Some keywords take a block as an argument. Blocks are grouped entities
of other keywords, and are delimited by curly braces ('{' and '}').
Namespaces are scoped within blocks:

namespace "core"
iface "eth0" {
	namespace "silly"
}
# the namespace is back to "core" at this point, not "silly".

5.2. Global keywords.
---------------------

- must/want and all/one/trip: these specify what interfaces to bring up
  at system boot time, or whenever the user says 'ifup auto'.
  In this context, 'must' will make 'ifup' exit unsuccessfully when
  bringing up the interfaces has failed, whereas 'want' will do no such
  thing. Using 'all' will cause the system to try to bring up all
  interfaces, whereas 'one' will stop after the first successfully
  brought up interface; and 'trip' will add event listeners so that if
  an interface later on goes down unexpectedly, the system will try to
  bring another interface up. Note that the latter requires daemon
  functionality (see below).
- (no) daemon: explicitly request or forbid the daemon. When using
  functionality that requires the daemon, the system will attempt to
  start the daemon; if 'no daemon' has been specified, however, this
  will fail, and ipcfg will exit with an error message. By using
  'daemon', the system will immediately start the daemon, regardless of
  whether any functionality in the config file requires it.
- plugins: specifies the list of plugins the system should load. Note
  that no functionality that is implemented by a plugin can be used
  before this keyword has been encountered.
- namespace: changes the default namespace. Contrary to any of the other
  keywords, this may be specified inside a block; namespaces are scoped
  within blocks, as specified above.

5.3. Configuration keywords
---------------------------

- iface: takes one string as an argument, and a block. The string
  contains the logical name of the interface we're about to configure;
  this may be something like 'eth0' or 'wlan1', but could just as well
  be 'home', 'work', or 'workvpn'. If doing that, however, you must make
  sure that the system somehow (through a plugin, or through the 'set'
  command; see below) gets the core:ifacename setting set.
- config: also takes one string and one block as an argument. The string
  refers to a configuration in the current namespace that can be
  included in iface blocks. Such a configuration need not be specified
  before the iface block in which it is specified; using it in the iface
  block is done using the 'config' keywords. Consider the following
  example:

namespace "core"
config "firewall:work" {
	action "firewall:flush" "full"
	group "firewall:table" "filter" {
		action "firewall:INPUT" ("m:state", "state:NEW,INVALID", "j:REJECT")
		action "firewall:FORWARD" ("j:REJECT")
	}
}

iface "work_wired" {
	set "ifacename" "eth0"
	config "firewall:work"
	config "core:dhcp4"
}

iface "work_wireless" {
	set "ifacename" "wlan0"
	config "firewall:work"
	# ... whatever is needed to set up the wireless interface goes
	# here
	config "dhcp4"
}

  The "core:dhcp4" configuration is predefined as what's needed to
  perform a DHCP request off an interface. The "firewall:work"
  configuration is the one that we specified in the "config" block.

  Plugins may predefine other "config" items. However, "config" does not
  take arguments; see "action".
  
  The argument to "config" is a list; i.e., it is possible to specify the
  "work_wired" example as follows:

iface "work_wired" {
	set "ifacename" "eth0"
	config ("firewall:work", "dhcp4")
}

- set: this will set a variable in the context that the system later can
  use. It will not have side-effects by itself.
- group: a blocked, scoped version of set. That is, the group in the
  "firewall:work" config in the above example could be rewritten like
  this:

	set "firewall:table" "filter"
	action "firewall:INPUT" ("m:state", "state:NEW,INVALID", "j:REJECT")
	action "firewall:FORWARD" ("j:REJECT")

  except that the group will also unset what it had previously set when
  the block is exited, while the "set" will do no such thing.
- action: this takes two arguments: a namespaced name, and a list. The
  name refers to something implemented by either the core system or by
  a plugin. It typically performs an atomic action, such as adding a
  rule to an interface, physically bringing up an interface, or
  something similar. It may receive arguments and have side-effects.
- require test: takes a string as an argument, and optionally a list.
  Performs a test, which may be successful or may fail; the list are
  arguments for the test. If the test fails, the system will abort
  what it was trying to do. An example could be:

iface "eth0" {
	require test "mii"
	config "dhcp4"
}

  If we are not connected to a network (which is what the 'mii' test
  tests for), it makes no sense to try to do DHCP, so abort.
- fail test: same as above, except that the result is inverted; that is,
  the system will abort what it was trying to do if the test succeeds,
  rather than if it fails.
- if: same as the previous two, except that it takes a block as an extra
  argument; if the test fails, the block is skipped. This allows for
  conditional statements. And 'if' may be nested.
  With 'if', many things are possible. For example, we can reimplement
  'want one':

iface "any_wired" {
	set "ifacename" "eth0"
	if fail test "mii" {
		set "ifacename" "eth1"
		test "mii"
	}
	config "dhcp4"
}

  Note that "set" is not scoped. In this example, if there is no cable
  connected to eth0, the system checks for eth1; if there is no cable
  connected there either, we abort. Next, we'll try doing DHCP off the
  interface we found in this manner.
