From 828fca691ee3778940eacfdec93af0e4c4ae2e0c Mon Sep 17 00:00:00 2001 From: Nikos Mavrogiannopoulos Date: Fri, 3 Apr 2026 13:37:29 +0200 Subject: [PATCH] radius: add group-separator option for OU= Class attributes Freeradius deployments may send groups in the Class attribute using a comma as separator (e.g. "OU=group1,group2") rather than the semicolon that ocserv expects by default. Add a group-separator option to the radius auth configuration to allow this to be changed: auth = "radius[config=...,group-separator=comma]" Accepted values are 'semicolon' (default) and 'comma'. The literal character is not accepted in the config syntax as it conflicts with the key=value pair delimiter. Resolves: #428 Signed-off-by: Nikos Mavrogiannopoulos --- NEWS | 3 +++ doc/README-radius.md | 5 +++++ doc/sample.config | 6 ++++-- src/auth/radius.c | 15 ++++++++++++--- src/auth/radius.h | 1 + src/common-config.h | 2 ++ src/subconfig.c | 13 +++++++++++++ 7 files changed, 40 insertions(+), 5 deletions(-) diff --git a/NEWS b/NEWS index 41a262ec..ce9f2c33 100644 --- a/NEWS +++ b/NEWS @@ -1,4 +1,7 @@ * Version 1.4.2 (unreleased) +- radius: added group-separator option to auth configuration, allowing + the separator used in OU= Class attributes to be set to 'semicolon' + (default) or 'comma', to support Freeradius deployments (#428) - The bundled llhttp was updated to 9.3.1. - occtl: Added 'terminate user', 'terminate id', and 'terminate session' commands that disconnect users and invalidate their session cookies, diff --git a/doc/README-radius.md b/doc/README-radius.md index 88aef64a..d49772cf 100644 --- a/doc/README-radius.md +++ b/doc/README-radius.md @@ -106,6 +106,11 @@ ATTRIBUTE Framed-Route 22 string # format "OU=group1;group2" or by a single group name # in the attribute. It is possible to specify multiple # groups in separate class attributes. +# The separator used in the OU= format defaults to ';' and can +# be changed via the group-separator option (accepted values: +# 'semicolon', 'comma'). For example, to use comma-separated +# groups as sent by Freeradius: +# auth = "radius[config=...,group-separator=comma]" # Note that this works only when groupconfig is set to # true, and if the groups sent by the server are made known # to ocserv, via the select-group config variable. diff --git a/doc/sample.config b/doc/sample.config index f9881e42..aff77731 100644 --- a/doc/sample.config +++ b/doc/sample.config @@ -28,11 +28,13 @@ # an oath password file to be used for one time passwords; the format of # the file is described in https://github.com/archiecobbs/mod-authn-otp/wiki/UsersFile # -# radius[config=/etc/radiusclient/radiusclient.conf,groupconfig=true,nas-identifier=name]: +# radius[config=/etc/radiusclient/radiusclient.conf,groupconfig=true,nas-identifier=name,group-separator=semicolon]: # The radius option requires specifying freeradius-client configuration # file. If the groupconfig option is set, then config-per-user/group will be overridden, # and all configuration will be read from radius. That also includes the -# Acct-Interim-Interval, and Session-Timeout values. +# Acct-Interim-Interval, and Session-Timeout values. The group-separator option +# sets the separator used in the OU= Class attribute; accepted values are +# 'semicolon' (default) and 'comma'. # # See doc/README-radius.md for the supported radius configuration attributes. # diff --git a/src/auth/radius.c b/src/auth/radius.c index 51b09eda..2e6e3dc4 100644 --- a/src/auth/radius.c +++ b/src/auth/radius.c @@ -98,6 +98,14 @@ static void radius_vhost_init(void **_vctx, void *pool, void *additional) vctx->nas_identifier[0] = 0; } + if (config->group_separator) { + strlcpy(vctx->group_separator, config->group_separator, + sizeof(vctx->group_separator)); + } else { + strlcpy(vctx->group_separator, ";", + sizeof(vctx->group_separator)); + } + if (rc_read_dictionary(vctx->rh, rc_conf_str(vctx->rh, "dictionary")) != 0) { fprintf(stderr, "error reading the radius dictionary\n"); @@ -233,10 +241,11 @@ static void append_route(struct radius_ctx_st *pctx, const char *route, } } -/* Parses group of format "OU=group1;group2;group3" */ +/* Parses group of format "OU=group1group2group3" */ static void parse_groupnames(struct radius_ctx_st *pctx, const char *full) { char *p, *p2; + const char *sep = pctx->vctx->group_separator; if (pctx->groupnames_size >= MAX_GROUPS) { oc_syslog( @@ -252,13 +261,13 @@ static void parse_groupnames(struct radius_ctx_st *pctx, const char *full) if (p == NULL) return; - p2 = strsep(&p, ";"); + p2 = strsep(&p, sep); while (p2 != NULL) { pctx->groupnames[pctx->groupnames_size++] = p2; oc_syslog(LOG_DEBUG, "radius-auth: found group %s", p2); - p2 = strsep(&p, ";"); + p2 = strsep(&p, sep); if (pctx->groupnames_size == MAX_GROUPS) { if (p2) diff --git a/src/auth/radius.h b/src/auth/radius.h index 2654807d..e84c3e22 100644 --- a/src/auth/radius.h +++ b/src/auth/radius.h @@ -35,6 +35,7 @@ struct radius_vhost_ctx { rc_handle *rh; char nas_identifier[64]; + char group_separator[2]; /* separator used in OU= Class attributes */ }; struct radius_ctx_st { diff --git a/src/common-config.h b/src/common-config.h index 9b01e8d8..dbc4439a 100644 --- a/src/common-config.h +++ b/src/common-config.h @@ -48,6 +48,8 @@ typedef struct gssapi_cfg_st { typedef struct radius_cfg_st { char *config; char *nas_identifier; + /* separator character(s) used in OU= Class attributes */ + const char *group_separator; } radius_cfg_st; typedef struct plain_cfg_st { diff --git a/src/subconfig.c b/src/subconfig.c index 8432ee89..78ea62ad 100644 --- a/src/subconfig.c +++ b/src/subconfig.c @@ -250,6 +250,19 @@ void *radius_get_brackets_string(void *pool, struct perm_cfg_st *config, 0) { additional->nas_identifier = vals[i].value; vals[i].value = NULL; + } else if (strcasecmp(vals[i].name, + "group-separator") == 0) { + if (strcasecmp(vals[i].value, "comma") == 0) + additional->group_separator = ","; + else if (strcasecmp(vals[i].value, + "semicolon") == 0) + additional->group_separator = ";"; + else { + fprintf(stderr, + "unknown group-separator value '%s'; use 'semicolon' or 'comma'\n", + vals[i].value); + exit(EXIT_FAILURE); + } } else if (strcasecmp(vals[i].name, "groupconfig") == 0) { if (CHECK_TRUE(vals[i].value))