The language used in the behaviour file is quite simple: it has no concept of
statements, blocks, or function calls. The whole behaviour file consists of one
single expression, and the only things the compiler knows about are contexts,
terms and operators.
As far as overall syntax goes, comments are started by '#' and ended by a
linefeed; whitespace is not important other than to make a difference between
cases such as 'Framed-Protocol' which refers to the attribute, and 'Framed -Protocol', which refers to an attribute called 'Framed', the subtraction operator,
and an attribute called 'Protocol'; and a term can not be followed
directly by another term.
The expression can have side effects on two lists of attribute/value
pairs: the REQUEST list, which contains every piece of information that is
known about a request, including RADIUS attributes, protocol fields, timestamp
and source and destination addresses and ports; and the REPLY list, which is
used to build the server's response when the expression is completed.
The expression may freely read and write, add, replace and delete pairs on
It may also make calls to external modules; this causes all pairs that are
on the REQUEST list at that point in time and that are allowed by the
interface's send ACL, to be transmitted an idle subprogram that's started for
While transmitting and waiting for the answer to have fully arrived, the
current job is suspended. However, the server can still handle new requests
that come in and sending and receiving on other interfaces. See the
documentation about the module interface
for more details.
When the answer has arrived, all received pairs that are allowed by the
interface's receive ACL are added to the REPLY list, and execution of the
expression is resumed until it completes or makes another interface call.
The language's terms come in two categories: immediate values and attribute
The value of an immediate term is already known at compile time. When a term
looks like 123, 0x55aa, SLIP, PPP, 10.0.0.1, or "Hello!\n", the value it has is
known immediately after parsing the term (and possibly looking up a constant
value in the dictionary based on the 'attribute context' - see below).
Attribute references have no value at compile time, but are substituted
each time they are used by an operator, with the value that is held by a
particular instance (first or last) of a particular attribute (as defined in
the dictionary) on a particular list (REQUEST, REPLY, the operator's default
or its opposite).
If a term is allowed in the current context, the first thing the compiler does
is to check if the next character is one that introduces a term of a
particular type, like this:
If a term starts with a decimal digit (or minus sign), the compiler
tries to parse it as a dotted-decimal IP address first (n.n.n.n;
0 <= n <= 255; see meta_atoip() in metatype.c).
If that doesn't succeed, it tries to parse it as a normal numeric value.
The following rules apply for that (see meta_atoord() in metatype.c):
if the number starts with a minus (-), the resulting value is
if the value starts with 0x, the rest is interpreted as a
hexadecimal value; the number stops at the first character that's not
a hexadecimal digit (between 0..9 or
A..F / a..f, inclusive);
if the value starts with a zero without a following 'x', the
rest is interpreted as an octal value; the number stops at the first
character that's not between 0 and 7, inclusive.
Parsing this term as a numeric value will always succeed, because the
compiler only tries to do so after it has seen a digit in the first place.
If a term starts with a single (') or double (") quote,
the compiler parses it as a string, up to the first non-escaped quote
of the same type. All other characters are taken as-is, with the exception
of the backslash (\), which starts an escape sequence (see
meta_prttoa() in metatype.c; the same function is also used when receiving
non-hex strings from external modules that use the ASCII interface).
Escape sequences are similar to those used in C. The following rules apply:
\n is interpreted as a linefeed;
\r is interpreted as a carriage-return;
\xNN is interpreted as a character with value NN in
hexadecimal. NN is either one or two digits, so
"\xyz" is an error and ends the string, "\x9z" is an
ASCII TAB followed by the letter 'z' and "\x41123" is the same
\NNN is interpreted as a character with value NNN in
octal. NNN is either one, two or three octal digits (each
between 0 and 7, inclusive).
If any other character than n, r, x or an
octal digit follows the backslash, it's copied as-is. Eg.
"C:\\COMMAND.COM" will return a valid DOS path.
Before checking for named constants as immediate values, the compiler first
scans the operator table to see if it can match an operator to the text
ahead, but if no operator is found that is valid in this context, it takes
as many characters as possible, as long as they are from the set
A..Z, a..z, 0..9,
semicolon (;) and minus (-).
Then, it first tries to find an attribute using that string (see Attribute
reference syntax under 2.2 below for more details).
Only if no attribute is found either, it tries to find a named constant
that is listed in the dictionary for the attribute that was last referenced in the same or a higher-level subexpression.
For example, this means that if you'd write:
Service-Type = ((Framed-Protocol = PPP), Login),
the name 'PPP' is searched for as a constant value for the Framed-Protcol
attribute, but 'Login' is searched for as a constant value for the
Service-Type attribute, not for Framed-Protcol. This is because the
subexpression 'Framed-Protocol = PPP' is at a lower level, and is closed
before the constant 'Login' is used. To quote a Perl manpage: "This may
seem a little weird, but that's ok, because it is weird."
You can think of an attribute reference as kind of a variable, although
it's not the sort of variable that always keeps its value until you change it
explicitly; it actually refers to a location where a particular
variable can be instead of an actual one.
The difference can be seen here: suppose you have have three instances of an
attribute called 'int' on the request list, having values 22, 33 and 44,
respectively. At this point, 'int' refers to the instance that has the value 44.
But after the subexpression 'del int' (which deletes the last instance of the
'int' attribute on the request list), 'int' will refer to the instance that is
now the last one, which has the value 33.
Attribute reference syntax
Attribute references are written like this (square brackets denote optional
[REQ: or REP:][F:]attribute specification
| | |
list override | [space:][vendor:]name
'first instance' flag
A list override, if present, specifies that the referenced attribute must be
searched for on the specified list, regardless of the default for the operator
that uses it as an operand. Most operators use the REQuest list by default;
generally only the ones that 'write' values have the REPly list by default,
such as '=' (add pair), ':=' (replace/add pair) and 'del' (delete pair).
If the 'first instance' flag is present, it specifies that the first matching
attribute on the referenced list is to be used. If not present, the last
instance of the attribute is used.
Within the attribute specification, the space and vendor names are optional -
most attribute names are not ambiguous anyway. See the dictionary for more
details about spaces and vendors.
One important note about the attribute name: if no list override is present,
i.e. when the list that is used depends on the operator, the following rule
applies: if the name starts with a lowercase letter, the list that's
not the operator's default is used.
Framed-IP-Address / 28 == 192.168.5.32 masks the last 4 bits of
the last instance of the Framed-IP-Address attribute on the REQUEST list and
compares the result to the IP address shown.
Service-Type = Login refers to the last instance of the
Service-Type attribute on the REPLY list, causing the '=' operator to add an
instance with constant value 'Login' that's defined for this attribute, at the
bottom of that list.
int = 123 adds an 'int' attribute with value 123 at the bottom of
the REQUEST list (inversed default for = operator because lowercase attribute).
REQ:User-Name := User-Name afterfirst "@" . "-" . User-Name beforefirst
"@" replaces the contents of the last instance of the User-Name
attribute on the REQUEST list with the contents of the same instance of the
same attribute after the occurance of the first '@', followed by a '-' and by
the contents of the same instance before the first '@'.
Unixpasswd(str:=User-Name), int && Reply-Message="Ok" will
replace the value of the last instance of the 'str' attribute on the REQUEST
list (or add the instance at the bottom if no 'str' was present yet) with the
contents of the last instance of the 'User-Name' attribute on the REQUEST list,
calls the interface Unixpasswd, tests the last instance of the 'int' attribute
on the REPLY list for boolean true and only adds an instance of Reply-Message
with the value 'Yes' at the bottom of the REPLY list if it is. Heh. Try to read
that in one breath.
A term is always of one of the four data integer, IP address, date or string.
For immediate value terms, the type is apparent immediately, just as the value.
For attribute reference terms, the type depends on how the attribute is defined
in the dictionary.
After the compiler has seen a term of a particular type, it sets the
current context to that type. The difference with contexts in
Perl is that there, operators set a context which can make terms behave
differently; here, terms set a context, which can make operators behave
differently (or better yet, which can select among differently behaving
operators). Eg. 170 ^ 255 (numeric bitwise xor) does something
different than "AaCgE" ^ " \x03" (stringwise xor).
Even though the language is indeed strongly-typed, the operators'
auto-conversion properties that are described below keep you from
worrying too much about that. Eg. although the '+' operator is allowed
in any context, it causes its surrounding terms to be converted to
integers first; "123" + "1" will return 124 (numeric, because that's the
type that '+' returns, but of course if this term is again used by an
operator that requires a string, auto conversion will happen again,
You'll see this type of operator behaviour more often than 'overloaded'
behaviour, because I generally think overloading should be used only
moderately. With auto-conversion, it's generally more obvious what the
operator in question actually does and returns.
Note that the context for an operator is only defined by the type of the
subexpression on its left: at the time the operator is searched for, the
right term's type is not known yet.
If no term was encountered yet in the current subexpression, the context is
'none'. (This is also the only context where terms are allowed - that's why no
term can directly follow another. Basically only the opening paren '(' and the
comma operator reset the context to 'none'. The comma does this after the
subexpression is closed, which happens immediately because it's an unary
The operators valid in this context are the unary prefix operators, as in -3,
or hex "hello", etc. This also allows the minus sign to be used for both the
negation and substraction operators.
As said earlier, if the compiler doesn't see a term, it searches its operator
table for an operator that matches the current context.
After an operator is found, the compiler looks at its precedence to see if the
current subexpression needs to be closed or not. Eg. take the expression 1 + 3
* 4: when it has already done the 1 + 3, and it sees the '*', it knows it
should not close the 1 + 3, but apply the '* 4' first, because that operator
has a higher precedence than '+'.
And while compiling 2 * 3 + 4 at the point of the '+', it knows it has to close
the 2 * 3 subexpression first, because '+' has a lower precedence than the
If the operator is of equal precedence, what happens depends on the type of
operator, which can be left-associative (close the current subexpression
first), or right-associative (don't close; apply operator first).
E.g. 3 firstof 5 lastof "This is an example", will cause the 'lastof' to
be applied before the 'firstof', even though they have equal precedence; it
is a right-associating operator. No subexpression is closed here until the
compiler sees the comma (which has a very low precedence).
If you want to play around with the behaviour language, go to the language
subdirectory after building the server and type 'make testprogs'. This will
produce an executable called 'langtest', which prompts you for an expression,
shows the compiler output and the result of executing it in the VM.
As a reference, here is the full table of operators in order of precedence,
taken almost literally from language/langcompile.c, roughly divided in
categories: WARNING: This is outdated. Please refer to
Note: these operators will probably be renamed or otherwise restructured soon.
To make some sense out of it all, read "X asY" like "X viewed as Y". That's why
the same operator name is sometimes associated with both the conversion from
and to strings. I should probably make them NOPs and have them take advantage
of the standard operand auto-conversion feature instead. Or something.
As can be seen above, there are quite a number of operators that do nothing
but convert between the various data types. However, in most cases you
won't neeed to worry about them, as most type conversion happens automatically.
This works because operators expect certain types of terms around them, as
shown above in the Type (LHS) and Type (RHS) columns. Even if the operator can
be applied in any context, it can still specify that the term on its left must
be converted to a particular type first. Eg. the string concatenation operator
'.' is allowed in any context, but indeed requires a string on both sides.
Thus, 0xaa . "-abc", causes the numeric value 0xaa (or 170) to be converted to
a (decimal) string first, giving the result "170-abc".
The same goes for the right side: when a new subexpression is compiled on the
right side of an operator, the compiler is told that whatever the result is, it
must be converted to a particular type after it ends, because that's what the
So when the compiler has gotten to the point of the left paren in "abc-" . (12
+ 3), it knows that just after the coming subexpression is ended, the 'convert
to string' operator must be applied before the concatenation operator, giving
the result "abc-15" here.
Interface calls are just operators. This operator is allowed only in context
'none', i.e. as a unary prefix operator, has a very high precedence, doesn't
require any type for the subexpression on its right side (even ignores its
result), and returns nothing meaningful.
So, Interface(int=3, str="abc"), is equivalent to int=3, Interface(str="abc"),
and also to int=3, str="abc", Interface 0, - in each case, an 'int' and a 'str'
instance are first added to the request list and then the interface 'Interface'
It just *looks* kind of fancy - like some function call with named parameters,
it also behaves a little like that - but in reality, it isn't fancy at all.
Often, you'll see something like Gofind(str:=User-Name), int && ( ...
This calls the interface, and then applies the && operator to the last instance
of the 'int' attribute on the reply list. It looks a bit like testing a return
value, but in reality interface calls have no return value themselves.
Note also that the parens don't limit the scope of the assignments in any way;
after the call, you still have the pseudo-parameters in your request list, and
you still have to delete them explicitly if you want to get rid of them.
This is the working example file 'behaviour.sample-usersfile' that is
included in OpenRADIUS v0.9.10 It uses a flat ASCII table for shared
secrets and a Livingston-style users file for accounts and profiles.
# BEHAVIOUR - Expression that defines the server's operating rules
# This is compiled at startup and ran for every request that comes in.
# Upon entry, the REQUEST list of A/V pairs is already populated with
# information from and about the request. Upon exit, the REPLY list is
# used to build a response to send to the client.
# Other than the attributes / fixed fields you want to send, you need
# to set the first instance of the attribute 'Secret' (see subdicts/dict.
# internal) to the shared secret to be used for signing the response.
# You also need to set the first instance of the RAD-Authenticator
# attribute to the value that this attribute had in the original request;
# i.e. copying the attribute from the REQUEST list to the REPLY list.
# The same goes for the RAD-Identifier attribute and all instances of
# the Proxy-State attribute. See RFC 2865.
# Then, when the expression completes without being aborted,
# the server will build the packet based on the attributes
# on the REPLY list, so at first also putting in the original request
# authenticator as the response authenticator. It then signs the
# packet using the shared secret provided on the REPLY list, putting the
# signature over the original response authenticator, to create a valid
# RADIUS response.
# See openradius-language.html for a list showing all operators,
# with contexts, precedence, association and auto-conversion properties.
# Also read this to understand that Attribute = Attribute references two
# different attributes, and how REQ: and REP: influence that.
# The && and || operators do short-circuit boolean evaluation as they do
# in C, Perl and shell scripts - that's how conditional subexpressions
# are implemented. 'and' and 'or' are synonyms.
# First, look up the client's secret by packet's source IP address;
# log an error and drop the request if not found
# Set the first 'str' instance to IP-Source (type conversion is automatic),
# call Clients and evaluate the returned 'str' instance as a boolean
# (false if nonexistant or empty).
Clientsfile(str = IP-Source),
str or (
Log-Line = "Request received on " . IP-Dest . ":" . UDP-Dest .
" from unknown client " . IP-Source . " identified as NAS " .
(NAS-Identifier or NAS-IP-Address) . " for user " . User-Name,
# Save returned string attribute in REP:Secret and delete the 'str'
# attribute used as parameter for the Clientsfile call and the attributes
# that were returned. Note that the 'del' operator has the REQUEST list
# as default for lowercase attributes; it's a 'write' operator after all.
Secret = str, del str, del REP:int, del REP:str,
# Now we can create legitimate responses, initialise the reply list
RAD-Code = Access-Reject,
RAD-Identifier = RAD-Identifier,
RAD-Authenticator = "" . RAD-Authenticator, # we need a copy
# Create the start of a log line. We add to it as we proceed below.
# The response type and RADIUS code are added by the server.
Log-Line = "from " . IP-Dest . ":" . UDP-Dest . " for request from NAS " .
(NAS-Identifier or NAS-IP-Address) .
(NAS-Port exists and (" port " . NAS-Port)) .
" via " . IP-Source . " for " . User-Name .
(Calling-Station-Id and (" CLI " . Calling-Station-Id)),
# Add NAS-dependent data
Nasesfile(str = (NAS-Identifier or NAS-IP-Address)),
del str, del REP:int,
# Add realm-dependent data. Also used to find local realms, i.e. realms
# that should be stripped before we process things further. I strongly
# believe in the home server stripping, not the upstream proxy, so that
# the home server can distinguish among multiple services.
str = (User-Name beforefirst "/" or User-Name afterlast "@"),
REQ:str and (
Realmsfile 0, del REP:int,
local-realm and REQ:User-Name := (User-Name afterfirst "/" or
User-Name beforelast "@" or
# We now take one of two separate paths that later join again,
# depending on whether we received an accounting request or not.
RAD-Code != Accounting-Request and (
# This is for authentication. Decrypt PAP password, if any;
# set CHAP-Challenge by copying it from the request authenticator
# if we're doing CHAP and it wasn't already there
User-Password and (
REQ:User-Password := (md5 (REP:Secret . RAD-Authenticator) ^ User-Password
. "\x00") beforefirst "\x00",
Log-Line := REP:Log-Line . " [" . User-Password . "]"
CHAP-Password and (
CHAP-Challenge or (REQ:CHAP-Challenge = RAD-Authenticator)
# Check for hardcoded (backdoor) users. Just a few examples
# that must of course be commented out when in production. This may
# also be useful to allow a telecommuting administrator in when the
# real backend database is down.
#(User-Name == "evb" and User-Password == "pingping" or
# User-Name == "emile" and User-Password == "pingping" or
# User-Name == "evbergen" and User-Password == "pingping") and (
# Reply-Message := "You rang, milord?\n",
# Service-Type = Administrative,
#User-Name == "backdoor" and User-Password == "user" and (
# Reply-Message := "Welcome, backdoor user.\n",
# Service-Type = Framed,
# Framed-Protocol = PPP,
#User-Name == "staff" and User-Password == "member" and (
# Reply-Message := "Hi, staff member. You're coming in on" .
# " NAS " . (NAS-Identifier or NAS-IP-Address) .
# " port " . NAS-Port . ".\nEnjoy.\n",
# Service-Type = Administrative,
# Find user in users file and get attributes from it. Reject right
# here if REP:auth-type is Reject, and accept without checking
# passwords if REP:auth-type is Accept.
Usersfile(str = User-Name),
del str, del REP:int,
auth-type == Reject and reject,
auth-type == Accept and accept,
# Handle both supported authentication types (either PAP or CHAP)
# if the users file gave us a cleartext password
clear-password and (
# See if we're doing PAP
User-Password exists and (
# PAP: check and done
User-Password == clear-password and accept,
Reply-Message = "PAP authentication failed. Access denied.",
# See if we're doing CHAP
CHAP-Password exists and (
# CHAP: check and done
16 lastof CHAP-Password == md5 (1 firstof CHAP-Password .
clear-password . CHAP-Challenge)
Reply-Message = "CHAP authentication failed. Access denied.",
# Apparently neither, but the users file _did_ contain
# clear-password: reject user
# Handle Md5-Hex style hashed password (PAP only) if the users
# file returned a md5-hex-password attribute.
# A cleartext PAP password is checked against a stored md5-hex-password by
# adding the first 4 octets of the md5-hex-password (the salt) to the PAP
# password, calculating md5 over the whole, converting the resulting 16
# octets to 32 hexadecimal digits and comparing those to the last 32 octets
# of the md5-hex-password.
# This is similar to the crypt(3) algorithm, but uses MD5 instead of DES and
# a salt up to 32 bits (24 when using the same charset, 16 when using only
# hexadecimal digits in the salt) instead of 12 bits.
REP:md5-hex-password exists and User-Password exists and (
32 lastof REP:md5-hex-password ==
hex md5 (User-Password . 4 firstof REP:md5-hex-password)
Reply-Message = "MD5-Hex authentication failed. Access denied.",
# Add other authentication schemes here.
1) or (
# Handle accounting. First verify request authenticator. Note that
# REP:RAD-Authenticator contains a saved copy of REQ:RAD-Authenticator
REQ:Acct-Authenticator = Mismatch,
REQ:RAD-Authenticator pokedwith "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0",
REP:RAD-Authenticator == md5 (RAD-Packet . REP:Secret)
and REQ:Acct-Authenticator := Verified,
# Next, create unique database key based on NAS, timestamp and session ID.
# No single NAS will reuse the same session ID in the same second.
REQ:Record-Unique-Key = hex (NAS-IP-Address toraw . Timestamp toraw) .
# Extend summary log line
Log-Line := REP:Log-Line . " signature " .
(Acct-Authenticator and "Verified" or "MISMATCH" ) . " key " .
# Always log it; drop request if that fails
Acctlogger(str = Timestamp as "%c"),
int or abort,
# If we weren't able yet to answer an authentication request, and for
# all accounting requests, we continue here.
# If somehow we obtained one or more Target-Server attributes, proxy.
REP:Target-Server exists and (
# for such things you'd want negative ACLs, but we don't have them yet.
# strip realm before proxying if asked to
strip-realm and REQ:User-Name := (User-Name afterfirst "/" or
User-Name beforelast "@" or
# proxy; drop request if radclient gave us a real error
Log-Line := REP:Log-Line . " proxied as " . User-Name . " to " .
Log-Line := REP:Log-Line . " resulting in " . REP:RAD-Code . " (" . int . ")",
int < 64 or abort,
# for such things you'd want negative ACLs, but we don't have them yet.
# We rely on the home server and our receive ACL to keep inappropriate
# attributes from rejects and accounting responses. If you want to be
# really sure that we follow our own dictionary in this respect, uncomment
# the following two lines.
#REP:RAD-Code == Accounting-Response and acctresp,
#REP:RAD-Code == Access-Reject and reject,
# Are you still here? We don't really know what to do,
# but these should be some sensible default actions for
# access requests and accounting requests.
RAD-Code == Accounting-Request and acctresp,
RAD-Code == Access-Request and reject,