On Wed, 13 Mar 2002, Miquel van Smoorenburg wrote:

> In article <Pine.LNX.4.33.0203131227520.1214-100000@maas16>,
> Marcus Zmieszkol  <[EMAIL PROTECTED]> wrote:
> >Hi radius-interested,
> >I've installed freeradius 0.4 under linux and radius is working well.
> >
> >1) Is it possible to limit the the amount of ip-addresses given out by the
> >radius server? So can I limit the amount of addresses?
> >
> >e.g. only 200 addresses should be give out from 10.252.172.0/24.
>
> Somebody needs to implement server-side pooling for that. See
> todo/serverside-ip-pools (not sure it it's in 0.4, it's in cvs for sure)
>
> Mike.
>
>
> -
> List info/subscribe/unsubscribe? See http://www.freeradius.org/list/users.html
>

Here it goes. Try it and tell me how it works.
Here is a sample configuration

        dhcp {
                session-db = ${raddbdir}/db.dhcp
                ip-index = ${raddbdir}/db.ip
                range-start = 192.168.4.5
                range-stop = 192.168.4.9
                cache-size = 10
        }

where:

session-db:     The db file containing the session database about ip<->nas/port
                allocations
ip-index:       A db file used as an index to implement multilink ppp
range-start
range-stop:     The ip range from which to allocate ips
chache-size:    The gdbm cache size. Should be around 1.5 * number of available
                ips

the dhcp module should be added to the authorize and accounting sections of
radiusd.conf

--
Kostas Kalevras         Network Operations Center
[EMAIL PROTECTED]      National Technical University of Athens, Greece
Work Phone:             +30 10 7721861
'Go back to the shadow' Gandalf
/*
 * rlm_dhcp.c
 *
 * Version:  $Id: rlm_dhcp.c,v 1.17 2002/02/25 18:47:55 aland Exp $
 *
 *   This program is free software; you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation; either version 2 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program; if not, write to the Free Software
 *   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 * Copyright 2001  The FreeRADIUS server project
 * Copyright 2002  Kostas Kalevras <[EMAIL PROTECTED]>
 */

#include "config.h"
#include "autoconf.h"
#include "libradius.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>

#include "radiusd.h"
#include "modules.h"
#include "conffile.h"

#include <gdbm.h>
#include <time.h>
#include <netinet/in.h>

#ifdef NEEDS_GDBM_SYNC
#       define GDBM_SYNCOPT GDBM_SYNC
#else
#       define GDBM_SYNCOPT 0
#endif

#ifdef GDBM_NOLOCK
#define GDBM_DHCP_OPTS (GDBM_SYNCOPT | GDBM_NOLOCK)
#else
#define GDBM_DHCP_OPTS (GDBM_SYNCOPT)
#endif

#ifndef HAVE_GDBM_FDESC
#define gdbm_fdesc(foo) (-1)
#endif

static const char rcsid[] = "$Id: rlm_dhcp.c,v 1.17 2002/02/25 18:47:55 aland Exp $";

/*
 *      Define a structure for our module configuration.
 *
 *      These variables do not need to be in a structure, but it's
 *      a lot cleaner to do so, and a pointer to the structure can
 *      be used as the instance handle.
 */
typedef struct rlm_dhcp_t {
        char *session_db;
        char *ip_index;
        uint32_t range_start;
        uint32_t range_stop;
        int cache_size;
        GDBM_FILE gdbm;
        GDBM_FILE ip;
        int fd;
        int ip_fd;
} rlm_dhcp_t;

typedef struct dhcp_info {
        uint32_t        ipaddr;
        char            active;
        char            cli[32];
} dhcp_info;

typedef struct dhcp_key {
        uint32_t nas;
        int port;
} dhcp_key;

/*
 *      A mapping of configuration file names to internal variables.
 *
 *      Note that the string is dynamically allocated, so it MUST
 *      be freed.  When the configuration file parse re-reads the string,
 *      it free's the old one, and strdup's the new one, placing the pointer
 *      to the strdup'd string into 'config.string'.  This gets around
 *      buffer over-flows.
 */
static CONF_PARSER module_config[] = {
  { "session-db", PW_TYPE_STRING_PTR, offsetof(rlm_dhcp_t,session_db), NULL, NULL },
  { "ip-index", PW_TYPE_STRING_PTR, offsetof(rlm_dhcp_t,ip_index), NULL, NULL },
  { "range-start", PW_TYPE_IPADDR, offsetof(rlm_dhcp_t,range_start), NULL, "0" },
  { "range-stop", PW_TYPE_IPADDR, offsetof(rlm_dhcp_t,range_stop), NULL, "0" },
  { "cache-size", PW_TYPE_INTEGER, offsetof(rlm_dhcp_t,cache_size), NULL, "1000" },
  { NULL, -1, 0, NULL, NULL }
};


/*
 *      Do any per-module initialization that is separate to each
 *      configured instance of the module.  e.g. set up connections
 *      to external databases, read configuration files, set up
 *      dictionary entries, etc.
 *
 *      If configuration information is given in the config section
 *      that must be referenced in later calls, store a handle to it
 *      in *instance otherwise put a null pointer there.
 */
static int dhcp_instantiate(CONF_SECTION *conf, void **instance)
{
        rlm_dhcp_t *data;
        int cache_size;
        dhcp_info entry;
        dhcp_key key;
        datum key_datum;
        datum data_datum;
        int i,j;
        char *cli = "0";
        
        /*
         *      Set up a storage area for instance data
         */
        data = rad_malloc(sizeof(*data));

        /*
         *      If the configuration parameters can't be parsed, then
         *      fail.
         */
        if (cf_section_parse(conf, data, module_config) < 0) {
                free(data);
                return -1;
        }
        cache_size = data->cache_size;

        if (data->session_db == NULL) {
                radlog(L_ERR, "rlm_dhcp: 'session-db' must be set.");
                free(data);
                return -1;
        }
        if (data->ip_index == NULL) {
                radlog(L_ERR, "rlm_dhcp: 'ip-index' must be set.");
                free(data);
                return -1;
        }
        data->range_start = htonl(data->range_start);
        data->range_stop = htonl(data->range_stop);
        if (data->range_start == 0 || data->range_stop == 0 || \
                         data->range_start >= data->range_stop ) {
                radlog(L_ERR, "rlm_dhcp: Invalid configuration data given.");
                free(data);
                return -1;
        }
        data->gdbm = gdbm_open(data->session_db, sizeof(int),
                        GDBM_WRCREAT | GDBM_DHCP_OPTS, 0600, NULL);
        if (data->gdbm == NULL) {
                radlog(L_ERR, "rlm_dhcp: Failed to open file %s: %s",
                                data->session_db, strerror(errno));
                return -1;
        }
        if (data->fd >= 0) data->fd = gdbm_fdesc(data->gdbm);

        data->ip = gdbm_open(data->ip_index, sizeof(int),
                        GDBM_WRCREAT | GDBM_DHCP_OPTS, 0600, NULL);
        if (data->ip == NULL) {
                radlog(L_ERR, "rlm_dhcp: Failed to open file %s: %s",
                                data->ip_index, strerror(errno));
                return -1;
        }
        if (data->ip_fd >= 0) data->ip_fd = gdbm_fdesc(data->ip);

        if (gdbm_setopt(data->gdbm, GDBM_CACHESIZE, &cache_size, sizeof(int)) == -1)
                radlog(L_ERR, "rlm_dhcp: Failed to set cache size");
        if (gdbm_setopt(data->ip, GDBM_CACHESIZE, &cache_size, sizeof(int)) == -1)
                radlog(L_ERR, "rlm_dhcp: Failed to set cache size");

        if (data->fd >= 0) rad_lockfd(data->fd, sizeof(int));
        key_datum = gdbm_firstkey(data->gdbm);
        if (data->fd >= 0) rad_unlockfd(data->fd, sizeof(int));
        if (key_datum.dptr == NULL){
                int rcode;

                DEBUG("rlm_dhcp: Initializing database");
                for(i=data->range_start,j=-1;i<=data->range_stop;i++,j--){
                        key.nas = 0;
                        key.port = j;
                        key_datum.dptr = (dhcp_key *) &key;
                        key_datum.dsize = sizeof(dhcp_key);

                        entry.ipaddr = ntohl(i);
                        entry.active = 0;
                        strcpy(entry.cli,cli);

                        data_datum.dptr = (dhcp_info *) &entry;
                        data_datum.dsize = sizeof(dhcp_info);

                        if (data->fd >= 0) rad_lockfd(data->fd, sizeof(int));
                        rcode = gdbm_store(data->gdbm, key_datum, data_datum, 
GDBM_REPLACE);
                        if (data->fd >= 0) rad_unlockfd(data->fd, sizeof(int));
                        if (rcode < 0) {
                                radlog(L_ERR, "rlm_dhcp: Failed storing data to %s: 
%s",
                                                data->session_db, 
gdbm_strerror(gdbm_errno));
                                free(data);
                                gdbm_close(data->gdbm);
                                gdbm_close(data->ip);
                                return -1;
                        }
                }
        }
        else
                free(key_datum.dptr);

        *instance = data;
        
        return 0;
}


/*
 *      Check for an Accounting-Stop
 *      If we find one and we have allocated an IP to this nas/port combination, 
deallocate it. 
 */
static int dhcp_accounting(void *instance, REQUEST *request)
{
        rlm_dhcp_t *data = (rlm_dhcp_t *)instance;
        datum key_datum;
        datum data_datum;
        int acctstatustype = 0;
        int port = -1;
        int rcode;
        uint32_t nas = 0;
        dhcp_info entry;
        dhcp_key key;
        int num = 0;
        VALUE_PAIR *vp;
        char str[32];


        if ((vp = pairfind(request->packet->vps, PW_ACCT_STATUS_TYPE)) != NULL)
                acctstatustype = vp->lvalue;
        else {
                DEBUG("rlm_dhcp: Could not find account status type in packet.");
                return RLM_MODULE_NOOP;
        }
        switch(acctstatustype){
                case PW_STATUS_STOP:
                        if ((vp = pairfind(request->packet->vps, PW_NAS_PORT_ID)) != 
NULL)
                                port = vp->lvalue;
                        else {
                                DEBUG("rlm_dhcp: Could not find port number in 
packet.");
                                return RLM_MODULE_NOOP;
                        }
                        if ((vp = pairfind(request->packet->vps, PW_NAS_IP_ADDRESS)) 
!= NULL)
                                nas = vp->lvalue;
                        else {
                                DEBUG("rlm_dhcp: Could not find nas ip address in 
packet.");
                                return RLM_MODULE_NOOP;
                        }
                        break;
                default:
                        /* We don't care about any other accounting packet */

                        return RLM_MODULE_NOOP;
        }

        key.nas = nas;
        key.port = port;
        key_datum.dptr = (dhcp_key *) &key;
        key_datum.dsize = sizeof(dhcp_key);

        if (data->fd >= 0) rad_lockfd(data->fd, sizeof(int));
        data_datum = gdbm_fetch(data->gdbm, key_datum);
        if (data->fd >= 0) rad_unlockfd(data->fd, sizeof(int));
        if (data_datum.dptr != NULL){

                /*
                 * If the entry was found set active to zero
                 */
                memcpy(&entry, data_datum.dptr, sizeof(int));
                free(data_datum.dptr);
                DEBUG("rlm_dhcp: Deallocated entry for ip/port: 
%s/%d",ip_ntoa(str,entry.ipaddr),port);
                entry.active = 0;

                data_datum.dptr = (dhcp_info *) &entry;
                data_datum.dsize = sizeof(dhcp_info);

                if (data->fd >= 0) rad_lockfd(data->fd, sizeof(int));
                rcode = gdbm_store(data->gdbm, key_datum, data_datum, GDBM_REPLACE);
                if (data->fd >= 0) rad_unlockfd(data->fd, sizeof(int));
                if (rcode < 0) {
                        radlog(L_ERR, "rlm_dhcp: Failed storing data to %s: %s",
                                        data->session_db, gdbm_strerror(gdbm_errno));
                        return RLM_MODULE_FAIL;
                }

                /*
                 * Decrease allocated count from the ip index
                 */
                key_datum.dptr = (uint32_t *) &entry.ipaddr;
                key_datum.dsize = sizeof(uint32_t);
                if (data->ip_fd >= 0) rad_lockfd(data->ip_fd, sizeof(int));
                data_datum = gdbm_fetch(data->ip, key_datum);
                if (data->ip_fd >= 0) rad_unlockfd(data->ip_fd, sizeof(int));
                if (data_datum.dptr != NULL){
                        memcpy(&num, data_datum.dptr, sizeof(int));
                        free(data_datum.dptr);
                        if (num >0){
                                num--;
                                DEBUG("rlm_dhcp: num: %d",num);
                                data_datum.dptr = (int *) &num;
                                data_datum.dsize = sizeof(int);
                                if (data->ip_fd >= 0) rad_lockfd(data->ip_fd, 
sizeof(int));
                                rcode = gdbm_store(data->ip, key_datum, data_datum, 
GDBM_REPLACE);
                                if (data->ip_fd >= 0) rad_unlockfd(data->ip_fd, 
sizeof(int));
                                if (rcode < 0) {
                                        radlog(L_ERR, "rlm_dhcp: Failed storing data 
to %s: %s",
                                                        data->ip_index, 
gdbm_strerror(gdbm_errno));
                                        return RLM_MODULE_FAIL;
                                }
                        }
                }
        }

        return RLM_MODULE_OK;
}

/*
 *      Find the named user in this modules database.  Create the set
 *      of attribute-value pairs to check and reply with for this user
 *      from the database. The authentication code only needs to check
 *      the password, the rest is done here.
 */
static int dhcp_authorize(void *instance, REQUEST *request)
{
        rlm_dhcp_t *data = (rlm_dhcp_t *) instance;
        int port = 0;
        int delete = 0;
        int rcode;
        int num = 0;
        uint32_t nas = 0;
        datum key_datum;
        datum nextkey;
        datum data_datum;
        dhcp_key key;
        dhcp_info entry;
        VALUE_PAIR *vp;
        char str[32];
        char str2[32];
        char *cli = NULL;

        /* quiet the compiler */
        instance = instance;
        request = request;

        /*
         * Get the nas ip address
         */
        if ((vp = pairfind(request->packet->vps, PW_NAS_IP_ADDRESS)) != NULL)
                nas = vp->lvalue;
        else{
                if ((vp = pairfind(request->packet->vps, PW_NAS_IDENTIFIER)) != NULL)
                        nas = vp->lvalue;
        }

        /*
         * Find the caller id
         */
        if ((vp = pairfind(request->packet->vps, PW_CALLING_STATION_ID)) != NULL)
                cli = vp->strvalue;

        /*
         * Find the port
         */
        if ((vp = pairfind(request->packet->vps, PW_NAS_PORT_ID)) != NULL){
                port = vp->lvalue;

                key.nas = nas;
                key.port = port;        
                DEBUG("rlm_dhcp: Searching for an entry for nas/port: 
%s/%d",ip_ntoa(str,nas),port);
                key_datum.dptr = (dhcp_key *) &key;
                key_datum.dsize = sizeof(dhcp_key);

                if (data->fd >= 0) rad_lockfd(data->fd, sizeof(int));
                data_datum = gdbm_fetch(data->gdbm, key_datum);
                if (data->fd >= 0) rad_unlockfd(data->fd, sizeof(int));
                if (data_datum.dptr != NULL){
                        /*
                         * If there is a corresponding entry in the database with 
active=1 it is stale.
                         * Set active to zero
                         */
                        memcpy(&entry, data_datum.dptr, sizeof(dhcp_info));
                        free(data_datum.dptr);
                        if (entry.active){
                                DEBUG("rlm_dhcp: Found a stale entry for ip/port: 
%s/%d",ip_ntoa(str,entry.ipaddr),port);
                                entry.active = 0;

                                data_datum.dptr = (dhcp_info *) &entry;
                                data_datum.dsize = sizeof(dhcp_info);

                                if (data->fd >= 0) rad_lockfd(data->fd, sizeof(int));
                                rcode = gdbm_store(data->gdbm, key_datum, data_datum, 
GDBM_REPLACE);
                                if (data->fd >= 0) rad_unlockfd(data->fd, 
sizeof(int));
                                if (rcode < 0) {
                                        radlog(L_ERR, "rlm_dhcp: Failed storing data 
to %s: %s",
                                                data->session_db, 
gdbm_strerror(gdbm_errno));
                                        return RLM_MODULE_FAIL;
                                }
                                /* Decrease allocated count from the ip index */

                                key_datum.dptr = (uint32_t *) &entry.ipaddr;
                                key_datum.dsize = sizeof(uint32_t);
                                if (data->ip_fd >= 0) rad_lockfd(data->ip_fd, 
sizeof(int));
                                data_datum = gdbm_fetch(data->ip, key_datum);
                                if (data->ip_fd >= 0) rad_unlockfd(data->ip_fd, 
sizeof(int));
                                if (data_datum.dptr != NULL){
                                        memcpy(&num, data_datum.dptr, sizeof(int));
                                        free(data_datum.dptr);
                                        if (num >0){
                                                num--;
                                                DEBUG("rlm_dhcp: num: %d",num);
                                                data_datum.dptr = (int *) &num;
                                                data_datum.dsize = sizeof(int);
                                                if (data->ip_fd >= 0) 
rad_lockfd(data->ip_fd, sizeof(int));
                                                rcode = gdbm_store(data->ip, 
key_datum, data_datum, GDBM_REPLACE);
                                                if (data->ip_fd >= 0) 
rad_unlockfd(data->ip_fd, sizeof(int));
                                                if (rcode < 0) {
                                                        radlog(L_ERR, "rlm_dhcp: 
Failed storing data to %s: %s",
                                                                        
data->ip_index, gdbm_strerror(gdbm_errno));
                                                        return RLM_MODULE_FAIL;
                                                }
                                        }
                                }
                        }
                }
        }
        /*
         * If there is a Framed-IP-Address attribute in the reply do nothing
         */
        if (pairfind(request->reply->vps, PW_FRAMED_IP_ADDRESS) != NULL)
                return RLM_MODULE_NOOP;

        /*
         * Walk through the database searching for an active=0 entry.
         */

        if (data->fd >= 0) rad_lockfd(data->fd, sizeof(int));
        key_datum = gdbm_firstkey(data->gdbm);
        while(key_datum.dptr){
                data_datum = gdbm_fetch(data->gdbm, key_datum);
                memcpy(&entry,data_datum.dptr, sizeof(dhcp_info));
                free(data_datum.dptr);  
                if (cli != NULL && strcmp(entry.cli,cli) == 0 && entry.active){
                        memcpy(&key,key_datum.dptr,sizeof(dhcp_key));
                        if (key.nas == nas)     
                                break;
                }
                if (entry.active == 0){
                        datum tmp;              

                        tmp.dptr = (uint32_t *) &entry.ipaddr;
                        tmp.dsize = sizeof(uint32_t);
                        if (data->ip_fd >= 0) rad_lockfd(data->ip_fd, sizeof(int));
                        data_datum = gdbm_fetch(data->ip, tmp);
                        if (data->ip_fd >= 0) rad_unlockfd(data->ip_fd, sizeof(int));

                        /*
                         * If we find an entry in the ip index and the number is zero 
(meaning
                         * that we haven't allocated the same ip address to another 
nas/port pair)
                         * or if we don't find an entry then delete the session entry 
so
                         * that we can change the key (nas/port)
                         * Else we don't delete the session entry since we haven't yet 
deallocated the
                         * corresponding ip address and we continue our search.
                         */

                        if (data_datum.dptr){
                                memcpy(&num,data_datum.dptr, sizeof(int));
                                free(data_datum.dptr);
                                if (num == 0){
                                        delete = 1;
                                        break;
                                }
                        }
                        else{
                                delete = 1;
                                break;
                        }
                }
                nextkey = gdbm_nextkey(data->gdbm, key_datum);
                free(key_datum.dptr);
                key_datum = nextkey;
        }
        if (data->fd >= 0) rad_unlockfd(data->fd, sizeof(int));
        /*
         * If we have found a free entry set active to 1 then add a Framed-IP-Address 
attribute to
         * the reply
         */
        if (key_datum.dptr){
                entry.active = 1;
                data_datum.dptr = (dhcp_info *) &entry;
                data_datum.dsize = sizeof(dhcp_info);

                if (delete){
                        /*
                         * Delete the entry so that we can change the key
                         */
                        if (data->fd >= 0) rad_lockfd(data->fd, sizeof(int));
                        gdbm_delete(data->gdbm, key_datum);
                        if (data->fd >= 0) rad_unlockfd(data->fd, sizeof(int));
                }
                free(key_datum.dptr);
                key.nas = nas;
                key.port = port;
                key_datum.dptr = (dhcp_key *) &key;
                key_datum.dsize = sizeof(dhcp_key);
                
                if (data->fd >= 0) rad_lockfd(data->fd, sizeof(int));
                rcode = gdbm_store(data->gdbm, key_datum, data_datum, GDBM_REPLACE);
                if (data->fd >= 0) rad_unlockfd(data->fd, sizeof(int));
                if (rcode < 0) {
                        radlog(L_ERR, "rlm_dhcp: Failed storing data to %s: %s",
                                data->session_db, gdbm_strerror(gdbm_errno));
                        return RLM_MODULE_FAIL;
                }

                /* Increase the ip index count */
                key_datum.dptr = (uint32_t *) &entry.ipaddr;
                key_datum.dsize = sizeof(uint32_t);     
                if (data->ip_fd >= 0) rad_lockfd(data->ip_fd, sizeof(int));
                data_datum = gdbm_fetch(data->ip, key_datum);
                if (data->ip_fd >= 0) rad_unlockfd(data->ip_fd, sizeof(int));
                if (data_datum.dptr){
                        memcpy(&num,data_datum.dptr,sizeof(int));
                        free(data_datum.dptr);
                }
                num++;
                DEBUG("rlm_dhcp: num: %d",num);
                data_datum.dptr = (int *) &num;
                data_datum.dsize = sizeof(int);
                if (data->ip_fd >= 0) rad_lockfd(data->ip_fd, sizeof(int));
                rcode = gdbm_store(data->ip, key_datum, data_datum, GDBM_REPLACE);
                if (data->ip_fd >= 0) rad_unlockfd(data->ip_fd, sizeof(int));
                if (rcode < 0) {
                        radlog(L_ERR, "rlm_dhcp: Failed storing data to %s: %s",
                                data->ip_index, gdbm_strerror(gdbm_errno));
                        return RLM_MODULE_FAIL;
                }
                        

                DEBUG("rlm_dhcp: Allocated ip %s to client on nas %s,port 
%d",ip_ntoa(str,entry.ipaddr),
                                ip_ntoa(str2,key.nas),port);
                if ((vp = paircreate(PW_FRAMED_IP_ADDRESS, PW_TYPE_IPADDR)) == NULL) {
                        radlog(L_ERR|L_CONS, "no memory");
                        return RLM_MODULE_NOOP;
                }
                vp->lvalue = entry.ipaddr;
                pairadd(&request->reply->vps, vp);
        }
        else{
                DEBUG("rlm_dhcp: No available ip addresses in pool.");
                return RLM_MODULE_NOOP;
        }

        return RLM_MODULE_OK;
}

static int dhcp_detach(void *instance)
{
        rlm_dhcp_t *data = (rlm_dhcp_t *) instance;

        gdbm_close(data->gdbm);
        gdbm_close(data->ip);
        free(data->session_db);
        free(data->ip_index);

        free(instance);
        return 0;
}

/*
 *      The module name should be the only globally exported symbol.
 *      That is, everything else should be 'static'.
 *
 *      If the module needs to temporarily modify it's instantiation
 *      data, the type should be changed to RLM_TYPE_THREAD_UNSAFE.
 *      The server will then take care of ensuring that the module
 *      is single-threaded.
 */
module_t rlm_dhcp = {
        "DHCP", 
        RLM_TYPE_THREAD_UNSAFE,         /* type */
        NULL,                           /* initialization */
        dhcp_instantiate,               /* instantiation */
        {
                NULL,                   /* authentication */
                dhcp_authorize,         /* authorization */
                NULL,                   /* preaccounting */
                dhcp_accounting,        /* accounting */
                NULL                    /* checksimul */
        },
        dhcp_detach,                    /* detach */
        NULL,                           /* destroy */
};

Reply via email to