10 Creating Custom Modules

This chapter describes how to create your own RADIUS module. Extending the RADIUS server is a task for advanced users with special requirements. Creating new modules requires C or C++ programming skills and an understanding of the RADIUS protocol and thread programming.

Caution:

Only administrators with advanced RADIUS skills should attempt to implement custom configurations.

Before creating a custom module you should be familiar with:

About Creating Custom Modules

Before creating your own module, be sure that the task you wish to accomplish cannot be performed by one of the modules supplied with the RADIUS Manager.

You may write a custom module which authenticates and modifies incoming packets to handle special conditions. You also might want to modify logins to provide greater flexibility in managing requests. With custom modules, you can:

  • Add or remove part of a login.

  • Authenticate a request against an alternate database.

  • Handle requests with identical logins.

Use mod_example as a template for your new module. Customize the required sections, and link your custom modules with the core functionality module libraries to produce new RADIUS server binaries.

Note:

The RADIUS manager framework does not expose the connection pool APIs to the custom RADIUS modules. Any custom RADIUS module that calls an opcode by calling the Connection Manager (CM) needs to manage its own connection pool.

Checklist for Creating Custom Modules

This checklist provides an overview of tasks that must be performed when you create a custom module. To create a custom module:

  1. Review the Module Class Model as implemented by RADIUS Manager. See "About the Module Class Model".

  2. After RADIUS Manager is installed, locate the mod_example template in BRM_home/source/apps/radius, where BRM_home is the directory in which BRM components are installed.

  3. Copy the mod_example.cpp and mod_example.h files. Store the copies in the BRM_home/source/apps/radius directory.

  4. Add the new module to the definitions file (BRM_home/source/apps/radius/moddef.cpp) file.

  5. Add functionality to the new module, by modifying the module.h and module.cpp files.

  6. Add an object for the new module to the Makefile:

    OBJS = moddef.o <mod_new>.o

  7. Build your custom module and link it with the libraries to produce a new radiusid executable:

    make -f makefile

    Note:

    When you run the makefile, the following warning message displays. You can ignore the warning.

    Warning 921: "/usr/include/stdlib.h", line 606 # A trailing comma is not allowed in an enumeration.

    AR_ARENA_OPEN=0,         /* open a session for obtaining memo

  8. Stop the RADIUS server, if it is running.

  9. Copy pin_radiusid from the current directory to BRM_home/bin.

  10. Edit the RADIUS configuration file (BRM_home/apps/radius/config) to add the new module to the module chain.

  11. Start the RADIUS server.

  12. Test the new module.

Modifying the Definitions File

Modify the definitions file (BRM_home/source/apps/radius/moddef.cpp) file by declaring the module master creation function and adding the name of your module to the module definition table.

Add this line:

{<mod_examplename>, MDF_NONE, RadiusModuleNew_create}

To this section of the moddef.cpp file:

....
    extern RadiusModule *RadiusModuleTransform_create(string theName, string theType,
    ModuleDefFlags theFlags);

    const ModuleConfigType module_config[] = {
    { "mod_null",     MDF_NONE,  RadiusModuleNull_create },
    #ifdef __unix
    { "mod_unixpwd",  MDF_NONE,  RadiusModuleUnixpwd_create },
    //{ "mod_ipass",    MDF_NONE,  RadiusModuleIPass_create },
    #endif
    { "mod_proxy",    MDF_NONE,  RadiusModuleProxy_create },
    { "mod_text",     MDF_NONE,  RadiusModuleText_create },
    { "mod_logging",  MDF_NONE,  RadiusModuleLogging_create },
    { "mod_transform",    MDF_NONE,  RadiusModuleTransform_create },
    { 0, MDF_NONE, 0 }
    { "mod_pin",      MDF_NONE,  RadiusModulePin_create },
};

Adding Functionality to a Custom Module

To add functionality to a custom module you must understand the C++ API interface and the Module Class Module.

About the C++ API Interface to RADIUS Modules

To create a new module type, declare new C++ classes which inherit from a set of base classes, and then implement the module type-specific functionality. The virtual base class for module masters is RadiusModule. See modbase.h for detailed information about the RadiusModule.

The module masters are instantiated by a special module definition table. See moddef.cpp for detailed information about the module definition table. The API support files contain specific information about using each file.

Support Header Files

The support header files (module.h) are self-documenting. See the following files for details:

In source/apps/radius:

  • moddef.h

In source/apps/radius/include/general:

  • applog.h - Application log functions

  • debuglog.h - Debug log functions

  • ipconvert.h - IP address translation utilities

  • list.h - List processing functions

  • lstring.h - String class

  • strutil.h - String processing functions

  • usersconfig.h - How to handle configuration files

In source/apps/radius/include/modules:

  • checksend.h - Check and send support for modules

  • modbase.h - Definitions of module classes

  • request.h - Incoming RADIUS request

  • stdconfig.h - Standard configuration keywords used in the configuration file

  • systemdict.h - Accessing the RADIUS dictionary

In source/apps/radius/include/radius:

  • attr.h - RADIUS attribute processing functions

  • dict.h - Dictionary processing functions

  • packet.h - RADIUS packet processing functions

  • password.h - Processing the RADIUS password attribute

  • structdatatype.h - Support for the struct data type in the dictionary

  • types.h - Enumerated values for attributes and their types

About the Module Class Model

Before writing a custom module, you must understand the Module Class Model and the support APIs.

This section describes the C++ API interface to module masters and module workers. A virtual base class inheritance model is used. This means that creating a new module type involves declaring new C++ classes which inherit from a set of base classes and then implement the module type's specific functionality.

About Configuring Modules

The modules, which are configured in the RADIUS configuration file (BRM_home/apps/radius/config), form an ordered list. When a request is received from the core server, it is processed by each module in turn until one of the modules indicates that it has completely processed the request. Note that the behavior of a module is determined by its configuration, especially its type.

Each module indicates what should happen next by setting a "return value":

  • MRT_CONTINUE indicates that the request should be passed to the next module.

  • MRT_COMPLETE indicates that the request has been successfully processed. The response has been filled in by the module and should be sent to the requesting client (NAS).

  • MRT_ERROR indicates that an error has occurred and the request should be discarded.

  • MRT_DISCARD indicates that the request should be discarded. When an individual module receives a request, it performs the following tasks:

    • Checks to see that the request matches its check sections. If not, it returns MRT_CONTINUE.

    • Optionally makes changes to the incoming request by adding, deleting, or modifying attributes. (See mod_transform for an example).

    • Optionally makes changes to the outgoing response by setting the response type, and adding, deleting, or modifying attributes.

    • Optionally adding attributes from any send sections to the outgoing response.

    • Returns MRT_CONTINUE, MRT_COMPLETE or MRT_ERROR as appropriate.

    • If the end of the module list is reached and no module has returned MRT_COMPLETE, the request is discarded.

      Note:

      Configure the mod_pin module after configuring all the other modules because mod_pin prepares the RADIUS response and sends it to the client.

About the Module Master

The virtual base class for module masters is RadiusModule. The salient parts are shown here from modbase.h. See modbase.h for detailed information.

class RadiusModule {
  protected:
    ModuleDefFlags flags;
    string name;
    string type;
    int ref_count;

    pthread_mutex_t mutex;
    ConfigEntry *shared_config;  // A copy of $CONFIG->(name)
    ConfigEntry *worker_config;  // A copy of $MODULES->(name)

    /*
    ** use()
    ** 
    ** Increments the reference count.
    */
    void use();

  public:
    /*
    ** RadiusModule()
    ** 
    ** Create a module master.
    ** The default operation intitialises name, type and flags from
    ** the parameters and sets the reference count to 1.
    */
    RadiusModule(string name, string type, ModuleDefFlags flags);

    /*
    ** ~RadiusModule()
    ** 
    ** Destroy the module master.
    ** Automatically called when the reference count reaches 0 (see use(), unuse())
    */
    virtual ~RadiusModule();

    /*
    ** unuse()
    ** 
    ** Decrement the reference count and if it reaches 0, destroys the object.
    */
    void unuse();

    /*
    ** newConfig()
    ** 
    ** Called when the configuration manager detects that
    ** new configuration information is available.
    **
    ** Default implementation searches for the entry $MODULES->(name),
    ** where (name) is the name of this module entry, and stores
    ** a copy of that part of the configuration tree in 'worker_config'.
    ** It also searches for $CONFIG->(name) and stores the result (if found)
    ** in shared_config.
    */
    virtual void newConfig(const ConfigEntry *top_config);

    /*
    ** createWorker()
    ** 
    ** User-defined function which creates a module worker.
    ** The worker object should take a COPY of its configuration,
    ** since during a reconfig, the worker may need to complete the
    ** current request before it dies.
    **
    ** Must call use() to increment the reference count on this object.
    */
    virtual RadiusModuleWorker *createWorker(int theThreadId) = 0;


   /*
    ** lock()
    ** unlock()
    ** 
    ** Synchronise access to this object.
    */
    void lock();
    void unlock();
  };

Methods Defined in Derived Classes for the Master Module

These methods may be defined in the derived class. See the source for mod_example as an example.

Constructor

You must provide a constructor in order to initialize the module master. The base class constructor must be called.

Example:

RadiusModuleExample::RadiusModuleExample(string name_, string type_, ModuleDefFlags flags_)
: RadiusModule(name_, type_, flags_)
{
}

Destructor

Destroys the object. Generally doesn't need to do anything.

Example:

      RadiusModuleExample::~RadiusModuleExample()
      {
      }

newConfig()

This method is called both at startup and when a reconfiguration event occurs. It is the responsibility of the module master to extract the appropriate configuration from the configuration tree. The default implementation keeps a reference to both the module type configuration (shared_config) and the per-module configuration (worker_config). Thus, most modules do not require a special implementation of this method.

createWorker()

This method creates a new module worker and increments the reference count. Typically, this method simply creates a new module worker of the appropriate type. The module worker is responsible for taking a copy of both the per-module configuration (worker_config) and the module type configuration (shared_config), when appropriate. It must also call use() to ensure that the reference count is incremented.

Example:

      RadiusModuleWorker *
      RadiusModuleExample::createWorker(int thread_id)
      {
      assert(worker_config != 0);
      use();
      return(new RadiusModuleExampleWorker(this, worker_config,
         shared_config, thread_id));
      }

About Worker Modules

The module worker is where most of the work of the module is accomplished.The virtual base class for module workers is RadiusModuleWorker. The salient parts are shown here from modbase.h. See modbase.h for detailed information.

Methods Defined in Derived Classes for Worker Modules

This section describes the APIs to module workers.

Constructor (required)

A constructor is required in order to initialize the module worker information.The base class constructor must be called to initialize the parent pointer and to copy the worker_config. Any configuration information that will be referenced must be copied. Specific configuration can also be extracted from the worker_config, the shared_config, or both. A CheckSend object is normally created here. Also, any standard options should be parsed here.

Example:

      RadiusModuleExampleWorker::RadiusModuleExampleWorker(RadiusModule *parent_,
      const ConfigEntry *worker_config,
      const ConfigEntry *shared_config,
      int thread_id)
      : RadiusModuleWorker(parent_, worker_config, thread_id)
      {
      checksend = new RadiusCheckSend(&config);
      action = config.getValue(MOD_EXAMPLE_ACTION);
      if (action == "") {
      APP_LOG(("[W]%s: No action specified in config. Using action=ignore.",
      (const char *)getName()));
      action = MOD_EXAMPLE_ACTION_IGNORE;
      }
      /* No shared_config for this module type */
      }

Destructor

Destroys the object.

Example:

      RadiusModuleExampleWorker::~RadiusModuleExampleWorker()
      {
      delete checksend;
      }

acceptRequest()

Processes the given request. The method:

  • May modify request->input if appropriate, by adding, modifying, or deleting attributes.

  • May modify request->output if appropriate, by setting the type, or adding, modifying, or deleting attributes.

  • Should call checksend->check() as the first thing, if appropriate, which should almost always be the case.

  • Should call checksend->addSendAttr() as the last thing when sending a response.

Returns one of MRT_..., such as:

      RadiusModuleWorker::ModuleReturnType
      RadiusModuleExampleWorker::acceptRequest(RadiusModuleRequest *request)
      {
      // Do standard check processing
      if (checksend->check(request->input, getSystemDict()) == 0) {
      return(MRT_CONTINUE);
      }
      DEBUG_LOG(("%s: check succeeded", (const char *)getName()));

      if (action == MOD_EXAMPLE_ACTION_DISCARD) {
      return(MRT_DISCARD);
      }

      if (action == MOD_EXAMPLE_ACTION_IGNORE) {
      return(MRT_CONTINUE);
      }

      if (action == MOD_EXAMPLE_ACTION_NAK) {
      switch (request->input->getType()) {
      case PW_ACCESS_REQUEST:
      request->output->setType(PW_ACCESS_REJECT);
      break;

      case PW_ACCOUNTING_REQUEST:
      request->output->setType(PW_ACCOUNTING_RESPONSE);
      break;

      default:
      return(MRT_DISCARD);
      }
      }
      else if (action == MOD_EXAMPLE_ACTION_ACK) {
      switch (request->input->getType()) {
      case PW_ACCESS_REQUEST:
      request->output->setType(PW_ACCESS_ACCEPT);
      break;

      case PW_ACCOUNTING_REQUEST:
      request->output->setType(PW_ACCOUNTING_RESPONSE);
      break;

      default:
      return(MRT_DISCARD);
      }
      }
      else {
      APP_LOG(("%s: unknown action: %s",
      (const char *)getName(), (const char *)action));
      return(MRT_ERROR);
      }

      checksend->addSendAttr(request->output, getSystemDict(),
      RadiusCheckSend::ADD_APPEND);

      return(MRT_COMPLETE);

Instantiating Module Masters

The sections above describe the APIs to module masters and module workers; however, they don't explain how module masters are instantiated.This is managed by a special module definition table.The excerpt below is from moddef.cpp:

    ....
    extern RadiusModule *RadiusModuleTransform_create(string theName, string theType,
    ModuleDefFlags theFlags);

    const ModuleConfigType module_config[] = {
    { "mod_null",     MDF_NONE,  RadiusModuleNull_create },
    #ifdef __unix
    { "mod_unixpwd",  MDF_NONE,  RadiusModuleUnixpwd_create },
    //{ "mod_ipass",    MDF_NONE,  RadiusModuleIPass_create },
    #endif
    { "mod_proxy",    MDF_NONE,  RadiusModuleProxy_create },
    { "mod_text",     MDF_NONE,  RadiusModuleText_create },
    { "mod_logging",  MDF_NONE,  RadiusModuleLogging_create },
    { "mod_transform",    MDF_NONE,  RadiusModuleTransform_create },
    { 0, MDF_NONE, 0 }
    { "mod_example", MDF_NONE,  RadiusModuleExample_create },
    { "mod_pin",      MDF_NONE,  RadiusModulePin_create },
};

Note:

Configure the mod_pin module after configuring all the other modules because mod_pin prepares the RADIUS response and sends it to the client.

This table associates Module Type names with functions that know how to create a module master of the associated class.You can modify this table to add custom modules.

Example of a module master creation function:

    RadiusModule *
    RadiusModuleExample_create(string name, string type, ModuleDefFlags flags)
    {
    return(new RadiusModuleExample(name, type, flags));
    }

This function creates an object of the appropriate type and returns it.

Note:

A separate function is required because it is not possible to take the address of a constructor in C++.

Sample Code for a Custom Module

This sample prints the name of the incoming RADIUS attributes and their values and appends the domain name to the user name attribute.

const RadiusAttr        *theAttr = NULL;
RadiusAttr * newAttr  = NULL;
/* This code prints the names of the incoming RADIUS atttributes and their valus */
DEBUG_LOG(("printing all attributes in the packet"));
while ((theAttr = request->input->getEntry ( theAttr )) != 0 ) {
string name = theAttr->printName(getSystemDict());
string value = theAttr->printValue(getSystemDict());
DEBUG_LOG(("attribute name = %s", (const char *)name));
DEBUG_LOG(("attribute value = %s", (const char *)value));

if (theAttr->getCode() <= PW_LAST_VALID_ATTR_CODE) {
newAttr  = new RadiusAttr (theAttr->getCode(),
theAttr->getBuffer(),
theAttr->getBufferLength());
request->output->addAttr ((RadiusAttr *)newAttr);
}

}

/* the following piece of code will append a domain name to the 
user-name attribute i.e if the username is joe, this code
will change it to joe@myisp.com, the 'add_domain' keywords
must be defined in your .h file and specified in the config file.
*/
DEBUG_LOG(("appending domain name to the user-name attribute"));
theAttr = request->input->getEntry ( PW_USER_NAME, NULL);
string value = theAttr->printValue(getSystemDict());
if (add_domain != "") {
DEBUG_LOG(("adding domain %s to username %s", add_domain.PeekString(), value.PeekString()));
value += add_domain;
RadiusAttr * newAttr  = new RadiusAttr (PW_USER_NAME, value);
const RadiusAttr * modAttr  = request->input->modifyAttr (theAttr, newAttr);
RadiusAttr  * outAttr  = new RadiusAttr (modAttr->getCode(), modAttr->getBuffer(), modAttr->getBufferLength());
request->output->addAttr ((RadiusAttr *)outAttr);

Adding a New Module to the RADIUS Configuration File

Edit the RADIUS configuration file (BRM_home/apps/radius/config) to add the new module to the module chain.

Example:

type=<mod_new>
status=enables
<check>
<module specfic actions>
<send>
<etc.)

Starting and Stopping the RADIUS Daemon

When you finish modifying the RADIUS configuration file you must restart the RADIUS daemon.

Use this procedure:

  1. Check to ensure that the RADIUS config file and dictionary file are in the directory BRM_home/apps/radius.

  2. Run either the start or stop script, BRM_home/bin/start_radius or BRM_home/bin/stop_radius. These scripts can be run manually but you should make them part of the software initialization at startup time.

  3. If you want pin_radiusd to start automatically when you restart the machine.

  4. Run the BRM_home/bin/install_radius script after you install the software. The install_radius script puts the required entries in the /etc/rc2.d directory to start pin_radiusd automatically.