not logged in | [Login]

Preamble

This article covers the current stable FreeRADIUS version 3.0.x.

The way modules are called in version 3 is very similar to version 2, but many of the internal API calls have changed.

This is a very light overview of what's required to create a module. Unfortunately, much of the internal FreeRADIUS API still lacks documentation. The easiest way to learn how to develop modules, is to read the code of existing modules and modify it to suit your own purposes.

There is also doc.freeradius.org which contains documentation for some functions and APIs.

Introduction

FreeRADIUS provides several different mechanisms to add site specific authentication and accounting procedures. The traditional approach was to create a shell script and execute it from the users file. This approach is still available in FreeRADIUS but has been deprecated in favour of unlang and interpreted language modules.

Execs require that FreeRADIUS fork another process and incur all associated process startup overhead each time an authentication or accounting request is received. Interpreted language modules must martial/unmartial values to and from the server core.

On a high volume server the overhead from either approach may become significant and impact response times. To eliminate this overhead you may create your own compiled modules. These modules are linked against the core server libraries, and loaded it into the server at runtime.

While creating new modules may sound like a lot of hassle, the process is actually fairly straight forward. The module, however does have to be written in C. If this idea does not sound palatable, and performance has become a critical issue, you may wish to request the development of the new module (via a feature request) on the GitHub issue tracker.

Alternatively, if the development of the module is required for a mission critical project, you may wish to sponsor its development. See the support tab on the http://www.freeradius.org site for more information.

Build system integration

Standalone Makefiles

boilermake is a framework for automatically managing make file targets, prerequisites and dependencies. It was written by Dan Moulding in GNU Make and is available here.

The version in FreeRADIUS is very similar, but has some additional functionality added by FreeRADIUS project lead Alan DeKok.

The boilermake build system used in v3.0.x makes it difficult to compile 3rd party modules separately from the server source. If you wish to maintain your module separately, a standalone Makefile will be required.

This Makefile should ensure that your module is linked both to libfreeradius-server and libfreeradius-radius as this will provide symbols during linking for all internal API functions.

The use of stand alone Makefiles will not be discussed further. The rest of the document assumes you will be using boilermake.

Integrating with boilermake

If you wish to maintain your module outside of the FreeRADIUS source tree, the recommended way of building your module is to include it in the src/modules directory using Git submodules. The boilermake build magic used for the rest of the modules will then also work with your module.

On make boilermake will automatically scan for, and include, files named all.mk in every directory under src/modules/, and build the requisite targets.

You don't need to worry about much of the internals of boilermake to integrate new modules into the build system, and no additional files outside of your module's directory need be modified to build your module.

If you will be using autoconf for dependency checks you should create an all.mk.in, and all.mk should be added to the .gitignore file in your module's directory. If your module has no dependencies a static all.mk file should be created instead.

In both instances, the following variables may be used to indicate which source files are to be included and which libraries need to be linked against.

Variable Defines
TARGETNAME Name of the module being built e.g. rlm_xxx.
TARGET Name of the static library file used to build the module shared library e.g. rlm_xxx.a. Usually derived from TARGETNAME.
SOURCES Source .c files required to build your module. Usually rlm_xxx.c but you may split your sources into several different files. See rlm_ldap for an example of this.
SRC_CFLAGS Any special CFLAGS required to build your module. Usually populated by an autoconf script, or omitted.
SRC_INCDIRS Additional directories to search for headers in. Usually omitted.
TGT_PREREQS Names of any FreeRADIUS internal libraries this module must be linked against (in addition to the base server library and protocol libraries). Common examples are libfreeradius-eap.a for EAP methods, and libfreeradius-redis.a for Redis related modules.
TGT_LDLIBS Linker arguments, and 3rd party libraries (-lfoo) this module should be linked against.

all.mk files need a minimum of TARGET and SOURCES defined.

As an example, this is the all.mk file for the rlm_always module.

TARGET		:= rlm_always.a
SOURCES		:= rlm_always.c

Using autoconf and boilermake together is more complex, and is easier to demonstrate than explain. Example / template files may be found here. The configure.ac and all.mk.in files will be of the most interest.

Note: When using autoconf to compile your configure.ac file, you should specify the FreeRADIUS base directory in the include path, this usually involves passing -I ../../../ to autoconf.

Module development

Getting started

Start by copying rlm_example.c and editing it to create your module.

Define a new struct (e.g. typedef struct rlm_xxx rlm_xx_t) to hold instance specific module configuration data.

/** Instance data struct for the rlm_example module
 *
 */
typedef struct rlm_example_t {
	bool		boolean;
	uint32_t	value;
	uint8_t const	*string;
	fr_ipaddr_t	ipaddr;
} rlm_example_t;

During module instantiation, the server will allocate structs of this type and pass them to the bootstrap and instantiation callbacks for population.

Unlike earlier versions of the server, in v3.0.x, the server core also takes care of applying the CONF_PARSER mappings (discussed below). When the module callbacks are executed, the instance data struct should already be populated with values from the configuration files.

Defining configuration items

The module_config array of CONF_PARSER structs is where the configuration items the module utilises are defined. There needs to be an element here for each configuration item used by the module.

Note there is also a special special list terminator element. This must be the last element in the array:

{ NULL, -1, 0, NULL, NULL }

Every other element has a .name for the configuration item, then a source .type which controls how the configuration item is parsed, an instance struct type, an .offset in that struct (where the parsed value should be written to), and a .dflt value for if the configuration item cannot be found.

This example maps the configuration item server and to the server field of a rlm_xxx_t struct:

{ "server", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_REQUIRED, rlm_xxx_t, server), NULL },

The matching configuration file entry would look like this:

modules {
  xxx {
    server = "server"
  }
}

Note If no server configuration item was found in the config the PW_TYPE_REQUIRED flag would cause the server to error out on startup. See the cf_item_parse documentation for other flags which may be |'d with PW_TYPE_ values.

The .type, .data and .offset fields should not be specified directly, but via the FR_CONF_OFFSET macro. This macro takes care of determining field offsets and performing compile time checking (that your PW_TYPE_STRING configuration item maps to a char const * field for example).

If a type mismatch is detected the compiler will produce an obscure error like the one below:

src/modules/rlm_example/rlm_example.c:51:14: error: initializing 'size_t'
      (aka 'unsigned long') with an expression of incompatible type
      'conf_type_mismatch' (aka 'void')
  { "string", FR_CONF_OFFSET(PW_TYPE_STRING, rlm_example_t, string), NULL },
        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
reeradius-server-fork/src/freeradius-devel/conffile.h:102:42: note:
      expanded from macro 'FR_CONF_OFFSET'
  ...FR_CONF_TYPE_CHECK((_t), __typeof__(&(((_s *)NULL)->_f)), offsetof(_s, _f))...
     ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
freeradius-server-fork/src/freeradius-devel/conffile.h:57:2: note:
      expanded from macro 'FR_CONF_TYPE_CHECK'
  __builtin_choose_expr((_t & PW_TYPE_TMPL),\
  ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

There is no useful information to be gleaned from the error other than the module_config array element which produced it. Use the documentation for the cf_item_parse function (which is responsible for performing the conversions) to determine the appropriate C data type to use for the field.

module_t struct

Every module must define a variable of type module_t matching the name of the module, and mark it as extern (it must not be static).

The module_t rlm_xxx {} definition establishes the connection between FreeRADIUS and the available services your module can provide.

This structure is referenced (using dlsym) after the server successfully loads the module's shared object (using dlopen).

If a symbol matching the name of the module cannot be found, or is missing the magic initialization data defined by RLM_MODULE_INIT, the server will error out and refuse to start.

The RLM_MODULE_INIT value is also used for cross platform version checking, to ensure that the modules and server are ABI compatible.

Other fields in the module_t are described by the code documentation for module_t.

This is an example module_t definition:

extern module_t rlm_xxx;
module_t rlm_xxx = {
	.magic		= RLM_MODULE_INIT,
	.name		= "xxx",
	.type		= RLM_TYPE_THREAD_SAFE,
	.inst_size	= sizeof(rlm_xxx_t),
	.config		= module_config,
	.bootstrap      = mod_bootstrap,
	.instantiate	= mod_instantiate,
	.detach		= mod_detach
	{
		[MOD_AUTHENTICATE]	= mod_authenticate,
		[MOD_AUTHORIZE]		= mod_authorize,
	},
};

See the documentation rlm_components for a complete list of the request processing callback functions that a module may provide.

Module functions

mod_boostrap

mod_boostrap is called very early during module initialization. If defined it should register any module specific attributes that that particular module instance will use during it's lifetime, and any xlats.

A good example is the rlm_sql module's bootstrap callback, which amongst many other things, creates a group attribute and registers a callback against it. This attribute is specific to each instance of rlm_sql as each instance of rlm_sql may connect to a different database.

static int mod_bootstrap(CONF_SECTION *conf, void *instance)
{
	...
	if (inst->config->groupmemb_query) {
		char buffer[256];

		char const *group_attribute;

		if (inst->config->group_attribute) {
			group_attribute = inst->config->group_attribute;
		} else if (cf_section_name2(conf)) {
			snprintf(buffer, sizeof(buffer), "%s-SQL-Group", inst->name);
			group_attribute = buffer;
		} else {
			group_attribute = "SQL-Group";
		}

		/*
		 *	Checks if attribute already exists.
		 */
		if (paircompare_register_byname(group_attribute, dict_attrbyvalue(PW_USER_NAME, 0),
						false, sql_groupcmp, inst) < 0) {
			ERROR("Error registering group comparison: %s", fr_strerror());
			return -1;
		}
		inst->group_da = dict_attrbyname(group_attribute);
	}
	...

	return 0;
}

If the module needs to load submodules (drivers or authentication mechanisms for example), this should also be done in the bootstrap callback.

mod_instantiate

mod_instantiate is called each time a new instance of a module is created during the initial startup process. Unlike previous versions of FreeRADIUS, parsing the module_config elements and allocating the module instance data struct are now performed by the server core, not the individual modules.

The work that should be performed in mod_instantiate is:

  • Sanity check the configuration provided by the user.
  • Populate any additional module instance data fields using derived data.
  • Initialise handles for databases (using the connection_pool API)
  • Get handles for other internal APIs (such as the exfile API).
  • Any other work that must be performed before the module is ready to handle requests.

Note that the instantiate function is not called each time a request is received by the server and passed to a module. The data established during boostrap and instantiation is shared between all requests at run time. If you need to store data that is associated with a particular request, and is valid only for the lifetime of a request, use the request_data_add and request_data_get functions.

Also note that if instance data is modified during request processing, a mutex, C11 atomics (v3.1.x only) or the PW_TYPE_UNSAFE flag must be used to prevent concurrent access.

mod_*

mod_authorize, mod_accounting, mod_authenticate and others all act in a similar way though they all perform very different operations. These are referred to as request time or request processing callbacks, and are only called to process requests, never during module instantiation.

See the examples in rlm_example for information on what operations should be completed by which request processing callbacks.

Thread safety

There is an obscure note near the end of rlm_example.c that talks about global variables. What that really is telling you is that your rlm_xxx.c module must store all data within the module's instance data structure. If you use global (non const) variables your module may compile and seem to run, but it will not do what you want.

Different instances of the same module should be unaware/unaffected by each others existence, other than possibly being aware of any 3rd party library initialization that's been performed. It is exceedingly rare that module instances sharing a mutex or global data is a sound architectural choice.

Also, keep in mind that in general, modules must be thread safe. There can be multiple threads using your module at any time. The RLM_TYPE_THREAD_SAFE flag tells FreeRADIUS that this is a thread-safe rlm. If you need to use a 3rd party library that cannot be made thread safe, then change RLM_TYPE_THREAD_SAFE to RLM_TYPE_THREAD_UNSAFE. FreeRADIUS will then permit only one instance of that to module to be called concurrently. Be advised that this will adversely affect performance and response times.

talloc

As of FreeRADIUS v3.0.0 we have moved memory allocation to talloc. talloc is a hierarchical memory allocation system, which allows you to define parent/child relationships between chunks of memory.

The parent in these relationships is known as a context. If a context is freed, all children parented by it are also freed. Creating relationships between memory chunks greatly simplifies the cleanup of complex structures such as REQUEST *.

Talloc is not always thread safe. Specifically; no two threads may modify a talloc hierarchy concurrently. If multiple threads do modify the talloc hierarchy concurrently, corruption of the talloc meta data may occur. Locks around the immediate context are not sufficient, modifications to the entire hierachy must be synchronized.

To work around this limitation, REQUEST * structures are allocated in the NULL context, meaning they are the top of their own talloc hierachy.

There are some general rules for which context to use:

  • Memory tied to the lifecycle of the request should be parented by REQUEST *request.
  • Memory tied to the lifecycle of the module should be parented by the module instance data.
  • Never allocate memory in threads (i.e. during request processing) in the context of the module instance data, or memory parented by it.

Also note that malloced memory cannot be used to parent talloced memory, and that talloced memory must be managed using talloc library functions only. It is not possible to call free() to free talloced memory, talloc_free() must be used instead.

Module Return Codes

There are a number of return codes that may be returned by the request processing callbacks.

The complete list is here, which is duplicated by the documentation for the rlm_rcodes enum.

Return Code Meaning
RLM_MODULE_REJECT This request is not permitted under local policy. FreeRADIUS responds immediately with a reject response.
RLM_MODULE_FAIL Processing of this request could not be completed. Something is not working properly. FreeRADIUS responds with a reject response.
RLM_MODULE_OK This request is permitted or this module has processed the request successfully. The FreeRADIUS response will be determined by processing of later modules. If this is the last module then the response will be an OK.
RLM_MODULE_HANDLED The module handled the request, so stop.
RLM_MODULE_INVALID The module considers the request invalid.
RLM_MODULE_USERLOCK Reject the request (user is locked out).
RLM_MODULE_NOTFOUND User not found.
RLM_MODULE_NOOP Module succeeded without doing anything.
RLM_MODULE_UPDATED OK (attributes modified).

Accessing module configuration data

Configuration parameters are written to a rlm_xxx_t instance data struct. A pointer to this structure is passed to each request processing function.

This must usually be cast to the correct type (it's a void *) before the fields can be accessed. The convention for casting is to declare a variable of the correct type as rlm_xxx_inst * inst, and assign instance to it.

Accessing Radius Request Attributes

v3.0.x uses a union the value_data_t struct to store all the possible data types for an attribute. The C data types do not necessarily match those used for module instance data. For example ipv4 prefixes are an 8 byte uint8_t array in value_data_t but a fr_ipaddr_t in module instance data structs.

This is mostly for legacy reasons - FreeRADIUS previously only supported RADIUS so it made sense to use data types which could be cast to offsets in a RADIUS packet.

You can see the evolution of the types here:

  • In Version 1 the ipaddr field was a character string (e.g., xxx.xxx.xxx.xxx).
  • In Version 2 the ipaddr field was a 4 byte binary value.
  • In Version 3 the ipaddr field is a struct in_addr.
  • In Version 4 the ipaddr field should be a fr_ipaddr_t.

The following example shows how you could access and print the values of the Framed-IP-Address attribute:

char buffer[INET_ADDRSTRLEN];
VALUE_PAIR *vp;
  
vp = fr_pair_find_by_num(request->packet->vps, PW_FRAMED_IP_ADDRESS, 0, TAG_ANY);
if (vp) {
  	RDEBUG("rlm_xxx: Found IP Address %s", inet_netop(AF_INET, &vp->vp_ipaddr, buffer, sizeof(buffer));
}

You may have noticed that in the above example we used vp_ipaddr to access the ipaddr field. There are a series of macros with a vp_ prefix, that expand to the correct field in the value_data_t part of the VALUE_PAIR struct.

This is to avoid excessive code changes every time we re-arrange VALUE_PAIR structures (which is surprisingly often) and to avoid lengthy chains of field accessors. The complete list can be found in libfreeradius.h

Here is a sample of the type macros available:

Data Type Content
vp_strvalue A pointer to an arbitrarily large string buffer.
vp_octets A pointer to an arbitrarily large binary string buffer.
vp_ip6addr An IPv6 address in struct in6_addr format
vp_ipv6prefix An IPv6 network.
vp_ether A 6 byte ethernet address in octet format.
vp_ipaddr An IPv4 address in struct in_addr format
vp_date A date value.
vp_integer An integer value in octet format.

Although the above example showed using the raw value from the value_data_t directly, it is usually considerably easier to use the provided VP prints functions.

vp_prints_value and vp_aprints_value will convert a VALUE_PAIR into its presentation format (the humanly readable format the server can re-ingest).

Here's an example of using vp_prints_value to convert the Framed-IP-Address attribute into its presentation format:

char buffer[INET_ADDRSTRLEN];
VALUE_PAIR *vp;
  
vp = fr_pair_find_by_num(request->packet->vps, PW_FRAMED_IP_ADDRESS, 0, TAG_ANY);
if (vp) {
	vp_prints_value(buffer, sizeof(buffer), vp, '"');
	RDEBUG("rlm_xxx: Found IP Address %s", buffer);
}

The standard radius dictionary items are prefixed by PW_. There's a build time process that converts the RFC attributes in the dictionaries into C preprocessor macros.

It's very rare that you'll need to access vendor specific attributes within your module.

If you do, the easiest way is via dict_attrbyname which takes in the string name of the attribute and resolves it (if it exists) to a DICT_ATTR entry in the global dictionary.

For legacy functions which don't accept DICT_ATTR pointers, the .attr and .vendor fields provide the necessary values.

Accessing RADIUS client additional attributes

Configuration for a RADIUS client defined in clients.conf can hold arbitrary-named additional attributes. These attributes may be used for various reasons, for example, to specify client's group name. These additional attributes are accessible though XLAT expansion in radiusd.conf ("%{client:...}"). C module can access these attributes using:

CONF_PAIR *mycp;
const char *value;
mycp = cf_pair_find(request->client->cs, "group");
value = cf_pair_value(mycp);
It extracts value of group attribute defined as:
client TESTCLIENT {
 ipv4addr       = 10.0.0.2
 secret         = mysupersecret
 nastype        = other
 group          = "mygroup"
}

Adding a reply attribute

The following shows how to add a reply attribute (one that will be returned to the NAS).

VALUE_PAIR *timeout;
sprintf (auth_msg, "%d", sestime * 60);

timeout = pairmake_reply("Session-Timeout", auth_msg, T_OP_SET);

The first argument to pairmake_reply is the attribute name. The second argument is its value, and the third argument is the operator. T_OP_SET is equivalent to := in unlang.

A full list of the operators can be found in the documentation for fr_token_t.

Creating debug and log entries

FreeRADIUS has a very helpful debug capability. You can certainly use trace commands like ktrace, strace, or truss. However, those are limited in what the show you about how your module is working. It is much easier to add debug statements into your module. If your FreeRADIUS is compiled with enable-debug then you can start it with:

radiusd -X

and it will generate lots of debug entries on the console to show you what it is doing. To add debug information to your modules you can use the following macro:

DEBUG("format string", variables);

The format string and variable list are the same as for a printf statement (and are validated by the compiler as such).

Use debug statements at critical places to show how the request is being processed.

If you're in a request processing function and have access to the REQUEST * use the RDEBUG family of macros instead. These will automatically prepend module instance data information to your debug messages.

In general logging macros with an R prefix are used to create messages relating to the current request, and those without an R prefix are used to create global log messages.

An example of a request specific error, might be that the value of an attribute in the requests doesn't match what the module expects.

An example of a global server error might be that there may be no connections left to a database.

Which log macros should be used where is well documented in the log.h header file.

Compiling Your Module

All you need to do when everything is correct is enter:

make
make install

Check errors very carefully. If you're not seeing your module being compiled, verify you ran the ./configure script for your module, and that it found all the required dependencies.

Including your module in an existing configuration

You need to make two modifications to the FreeRADIUS configuration files to add your module.

First you need to create a module configuration file in the raddb/mods-available directory and link it to raddb/mods-enabled.

The contents of the file should look something like:

xxx {
      server = "myhost"
}

This defines the configuration values for the module.

Next you'll need to list the modules and sections where you want the file to be called. You can only list your module in sections matching the request processing callbacks you included in your module_t struct.

If you try and list your module in the preacct section, but there's no mod_preacct function defined or included in the module_t the server will error out and refuse to start.

If you are only running one virtual server then you can use the default file in the sites-available directory. In that file you would add your module to the appropriate section. For example in this case:

# Authorization. First preprocess (hints and huntgroups files),
authorize {
      xxx
}

Testing

At this point you are ready to test the server. Make sure to start it with:

radiusd -X

Check through the output to be sure the module loaded and initialized itself properly.

If you're planning on contributing the module back to the FreeRADIUS project, please create unit tests to test various aspects of your module.

See the src/tests/modules directory in the server repository for examples.

Available Modules

See: List of modules

See Also

GitHub This article covers the current FreeRADIUS version 3.x.