0. Contents

1. General
2. Terms
    2.1. Immediate values
    2.2. Attribute references
3. Types and contexts
4. Operators
    4.1. Miscellaneous (unary prefix)
    4.2. Conversion (unary postfix)
    4.3. Arithmetic, boolean, string transformations (right-associative unary prefix)
    4.4. Arithmetic (left-associative binary)
    4.5. String operations (left- and right-associative binary)
    4.6. Comparing (left-associative binary)
    4.7. Assignment (right-associative binary)
    4.8. Boolean / flow control (short-circuit binary, unary postfix)
5. Automatic type conversion
6. Interface calls

Appendix A. Operator usage reference
Appendix B. Example behaviour file from OpenRADIUS release 0.9
 

1. General

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 any list.

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 this interface.

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.
 

2. Terms

The language's terms come in two categories: immediate values and attribute references.

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).

2.1. Immediate values

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:

2.2. Attribute references

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 components):

[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.

Some examples:

3. Types and contexts

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, giving "124").

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 postfix operator).

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.
 

4. Operators and precedence

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 current subexpression.

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:

NameContextPrecedenceType (LHS)Type (RHS)Ret.context

4.1. Miscellaneous (unary prefix)
haltnone32 (L)N.A.anyinteger
abortnone32 (L)N.A.anyinteger
delallnone32 (L)N.A.anyinteger
delnone32 (L)N.A.anyinteger
moveallnone32 (L)N.A.anyinteger

4.2. Conversion (unary postfix)

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.


asrawinteger30 (L)N.A.N.A.string
asrawIP addr.30 (L)N.A.N.A.string
asrawdate30 (L)N.A.N.A.string
asstrinteger30 (L)N.A.N.A.string
asstrIP addr.30 (L)N.A.N.A.string
asstrdate30 (L)N.A.N.A.string
asintIP addr.30 (L)N.A.N.A.integer
asintstring30 (L)N.A.N.A.integer
asintstring30 (L)N.A.N.A.integer
asipinteger30 (L)N.A.N.A.IP addr.
asipstring30 (L)N.A.N.A.IP addr.
ashexintstring30 (L)N.A.N.A.integer
ashexipstring30 (L)N.A.N.A.IP addr.
ashexstring30 (L)N.A.N.A.string
ashexany (other)30 (L)N.A.N.A.string
asymdhms *1string30 (L)N.A.N.A.date
asymdhmsdate / integer30 (L)N.A.N.A.string
asymd *1string30 (L)N.A.N.A.date
asymddate / integer30 (L)N.A.N.A.string
ashms *1string30 (L)N.A.N.A.date
ashmsdate / integer30 (L)N.A.N.A.string

4.3. Arithmetic, boolean, string transformations (right-associative unary prefix)
~none28 (R)N.A.integerinteger
-none28 (R)N.A.integerinteger
!none28 (R)N.A.anyinteger
md5none28 (R)N.A.stringstring
hexnone28 (R)N.A.stringstring

4.4. Arithmetic (left-associative binary)
*any25 (L)integerintegerinteger
/IP addr.25 (L)N.A.integerIP addr.
/any25 (L)integerintegerinteger
%any25 (L)integerintegerinteger
+any24 (L)integerintegerinteger
-any24 (L)integerintegerinteger
>>any23 (L)integerintegerinteger
<<any23 (L)integerintegerinteger
^integer22 (L)N.A.integerinteger
^IP addr.22 (L)N.A.IP addr.IP addr.
^string22 (L)N.A.stringstring
&any21 (L)integerintegerinteger
|any20 (L)integerintegerinteger

4.5. String operations (left- and right-associative binary)
asdate / integer18 (L)N.A.stringstring
cryptany16 (L)stringstringstring
beforefirstany16 (L)stringstringstring
afterfirstany16 (L)stringstringstring
beforelastany16 (L)stringstringstring
afterlastany16 (L)stringstringstring
firstofany16 (R)integerstringstring
lastofany16 (R)integerstringstring
.any14 (L)stringstringstring

4.6. Comparing (left-associative binary)
> *1string12 (L)N.A.stringinteger
>any (other)12 (L)integerintegerinteger
< *1string12 (L)N.A.stringinteger
<any (other)12 (L)integerintegerinteger
>= *1string12 (L)N.A.stringinteger
>=any (other)12 (L)integerintegerinteger
<= *1string12 (L)N.A.stringinteger
<=any (other)12 (L)integerintegerinteger
==string11 (L)N.A.stringinteger
==any (other)11 (L)integerintegerinteger
!=string11 (L)N.A.stringinteger
!=any (other)11 (L)integerintegerinteger

4.7. Assignment (right-associative binary)
=integer7 (R)N.A.integerinteger
=IP addr.7 (R)N.A.IP addr.IP addr.
=date7 (R)N.A.datedate
=string7 (R)N.A.stringstring
:=integer7 (R)N.A.integerinteger
:=IP addr.7 (R)N.A.IP addr.IP addr.
:=date7 (R)N.A.datedate
:=string7 (R)N.A.stringstring

4.8. Boolean / flow control (short-circuit binary, unary postfix)
&&any5 (R)anyanyrhs' type
||any4 (R)anylhs' typerhs' type
,any1 (L)anyN.A.none

Notes: *1 - Not implemented yet.
 

5. Automatic type conversion

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 operator expects.

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.
 

6. Interface calls

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' is called.

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.
 

Appendix A. Operator usage reference


 

Appendix B. Example behaviour file

This is the working example file 'behaviour.sample-usersfile' that is included in OpenRADIUS v0.9. 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 why.
#
# Then, when the expression completes (that is without being aborted),
# the server will first build the packet only based on the attributes
# on the REPLY list, so at first also putting in the original request
# authenticator as the response authenticator. It will then sign 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 neatly showing all operators, 
# with contexts, precedence, association and auto-conversion properties.  
#
# The && and || operators do short-circuit boolean evaluation as they do
# in C, Perl and shell scripts - that's how conditional subexpressions
# are implemented.
#
# This language has hardly any syntax, other than that a term may be
# not be directly followed by another term. 
#
##
#
# This is an expression that goes together with the distributed sample
# 'configuration' file. It allows RADIUS clients to be listed in an
# ASCII file .../raddb/legacy/clients, and users in .../raddb/legacy/users
# (paths are dependent on what's specified in the configuration file).
#
# More specifically, this file provides the following rules:
#
# 1.  look up the secret for the radius client by IP address, using the
#     Oldclients interface that uses the ascfile module to return
#     information from a flat file that lists the IP addresses and their 
#     secrets next to each other, normally .../raddb/legacy/clients;
#
# 2.  if not found, log the event using the Errorlogger interface that
#     uses the radlogger shell script module to log the message, normally
#     in /var/log/raderr.log and drops the request. Otherwise continue:
#
# 3.  initialise the reply list by copying the Identifier, Authenticator 
#     and all Proxy-State attributes from the request;
#
# 4.  see if we're accounting. If so, verify the request authenticator
#     and put the result in Acct-Authentic, log the information using
#     the Acctlogger interface that uses the radlogger module. If that
#     didn't succeed, drop the request; otherwise, set RAD-Code to
#     Accounting-Response and halt the expression so that the client is
#     answered. But if we're authenticating:
#
# 5.  set RAD-Code to Access-Reject and Reply-Message to something nasty,
#     so we can send a valid response that rejects users by simply 
#     halting the expression at any point;
#
# 6.  if a PAP password was supplied in the request, decrypt that
#     using the shared secret that was found for this client.
#     Otherwise, and if also a CHAP password was supplied, add a
#     CHAP-Challenge attribute with the value of the request
#     authenticator if there wasn't already one present in the request;
#
# 7.  log the request using the Stdlogger interface that uses the
#     radlogger shell script to log the username, password, CHAP
#     challenge and password, NAS IP and port, as per the send ACL
#     defined in the configuration file for the Stdlogger interface;
#
# 8.  check if this user is somebody who may always get in (backdoor)
#     when using a particular username and PAP password, if so, change
#     RAD-Code to Access-Accept, change Reply-Message to someting nice
#     and halt. Otherwise go on:
# 
# 9.  look up the user using the Oldusers interface that uses the
#     ascfile module to get reply attributes for the user from a
#     Livingston-style users file, normally .../raddb/legacy/users.
#     (Note that ascfile itself doesn't look at check items - it just
#     returns all attributes and the server will add them all to the
#     reply list. So any compares need to be done here);
#
# 10. See if the (internal) auth-type attribute is now present on the reply
#     list, after calling Oldusers. If it is and it has the value
#     Reject, halt as we're done. If it has the value Accept, set
#     RAD-Code to Access-Accept, delete the first (nasty) instance of
#     the Reply-Message attribute and halt, accepting the user with a
#     possible reply message set by the users file. Otherwise, ignore
#     the value and go on:
#
# 11. see if the clear-password attribute was set. If so and we have a
#     PAP password, just compare it (we decrypted it earlier) and if it
#     matches, we set RAD-Code to Access-Accept and delete the first
#     Reply-Message before halting, otherwise:
#
# 12. see if (the clear-password attribute was set and) we have a CHAP
#     password, if so, perform CHAP by comparing it to the
#     clear-password hashed with the CHAP-Challenge. If it matches, set
#     RAD-Code to Access-Accept and delete the first Reply-Message
#     before halting, otherwise:
#
# 13. if indeed the clear-password was set and we neither have a PAP nor
#     a CHAP password, reject the user;
#
# 14. if the md5-hex-password attribute was set, see if we have a PAP
#     password. If not, halt, otherwise, calculate md5 over the PAP
#     password followed by the first 4 characters of the
#     md5-hex-password, write the hash as hex, compare it to the last 32
#     characters of the md5-hex-password and if it matches, set RAD-Code
#     to Access-Accept and delete the first Reply-Message before
#     halting.
#
#     (Note that this looks quite like the crypt(3) algorithm, but using
#     a 16-bit salt, and using md5 instead of some DES hash.  This is
#     also a bit different from the md5 passwords supported in
#     /etc/passwd by some crypt implementations: there, base64 is used
#     instead of hex. But as I haven't implemented a base64 operator
#     yet, I can't support that here for now.
      

###########################################################################
#
# Step 1 & 2: Look up client's secret by packet's source IP address; log
# error and drop request if not found

# Set the first 'str' instance to IP-Source (type conversion is
# automatic), call Oldclients and evaluate the returned 'str' instance
# as a boolean (false if nonexistant or empty).

Oldclients(str=IP-Source), str || (

	Errorlogger(str=Timestamp as "%c" . ": Request from unknown client " . 
			IP-Source . " identified as NAS " . (NAS-Identifier || 
							     NAS-IP-Address) . 
			" for user " . User-Name . " - ignored."),
	abort
), 

# Save returned string attribute in REP:Secret and delete the parameter
# and the returned attributes. Note that the 'del' operator has the
# REQUEST list as default for lowercase attributes because of rule c.

Secret = str, del str, del REP:int, del REP:str,


###########################################################################
#
# Step 3: Initialise reply list

RAD-Identifier = RAD-Identifier, 
RAD-Authenticator = RAD-Authenticator,
RAD-Length = 20,			# Dirty hack. Is overwritten by
					# encode() if there are any real
					# attributes.

RAD-Code = Access-Reject,		# Another dirty hack, but needed
					# now to make sure all RAD-PKT
					# attributes appear in the reply
					# list before the ones from
					# RAD-ATR. buildtree() does not
					# yet sort on encapsulation
					# depth... :(
moveall Proxy-State,


###########################################################################
#
# Step 4: Handle accounting

RAD-Code == Accounting-Request && (

	# Verify request authenticator. Note that the ',1' in the
	# subexpression after the '&&' makes the '||' following it
	# behave like a logical 'else'.

	RAD-Authenticator == md5 (RAD-Code asraw . 
				  RAD-Identifier asraw .
				  RAD-Length asraw . 
				  "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" .
				  RAD-Attributes . 
				  REP:Secret) && (

		REQ:Acct-Authentic = Verified, 
	1) || (
		REQ:Acct-Authentic = Mismatch
	),

	# Always log it; drop request if that fails

	Acctlogger(str=Timestamp as "%c",
		   REQ:Record-Unique-Key=NAS-IP-Address asraw ashex .
					 Timestamp asraw ashex .
					 Acct-Session-Id), int || abort,
	del str, del REP:int,
	
	# Set response code and halt the expression

	RAD-Code := Accounting-Response,
	halt
),


###########################################################################
#
# Step 5: Now we're authenticating, set defaults to reject when halted,
# so we can do so at anytime

# RAD-Code = Access-Reject, # Not needed anymore - already done 
Reply-Message = "I don't know you. Go away.\n",


###########################################################################
#
# Step 6: Decrypt PAP password or if we're doing CHAP, set
# CHAP-Challenge by copying it from the request authenticator if it
# wasn't already present 

User-Password && (

	REQ:User-Password := User-Password ^ md5 (REP:Secret . 
						  RAD-Authenticator)
			     beforefirst "\x00"
),

CHAP-Password && (

	CHAP-Challenge || (REQ:CHAP-Challenge = RAD-Authenticator)

),


###########################################################################
#
# Step 7: Log the request using the Stdlogger interface

Stdlogger(str=Timestamp as "%c"), 
del str, del REP:int,


###########################################################################
#
# Step 8: Check for hardcoded (backdoor) users

(User-Name == "evb" && User-Password == "pingping" || 
 User-Name == "emile" && User-Password == "pingping" || 
 User-Name == "evbergen" && User-Password == "pingping") && (

	RAD-Code := Access-Accept,
	Reply-Message := "You rang, milord?\n",
	Service-Type = Administrative,
	halt
),

User-Name == "backdoor" && User-Password == "user" && (

	RAD-Code := Access-Accept,
	Reply-Message := "Welcome, backdoor user.\n",
	Service-Type = Framed,
	Framed-Protocol = PPP,
	halt
),

User-Name == "staff" && User-Password == "member" && (

	RAD-Code := Access-Accept,
	Reply-Message := "Welcome, staff member. You're coming in on NAS " . 
			 (NAS-Identifier || NAS-IP-Address) . " on port " . 
			 NAS-Port . ".\nEnjoy.\n",
	Service-Type = Administrative,
	halt
),


###########################################################################
#
# Step 9 & 10: Find user in legacy-style users file and get attributes
# from it. Reject if not found. Also reject right here if REP:auth-type
# is Reject, and accept without checking passwords if REP:auth-type is
# Accept.

Oldusers(str=User-Name), int || halt,
del str, del REP:int,

auth-type == Reject && halt,
auth-type == Accept && (

	RAD-Code := Access-Accept,
	Reply-Message := "Welcome, user with any password.\n",
	halt
),


###########################################################################
#
# Step 11, 12 and 13: Handle supported authentication types if we have a
# cleartext password (that is either PAP or CHAP). 

clear-password && (

	# See if we're doing PAP

	User-Password && (

		# PAP: check and done

		User-Password == clear-password && (

			RAD-Code := Access-Accept,
			Reply-Message := "Welcome, PAP/Login user.\n"
		),

		halt
	),

	# See if we're doing CHAP

	CHAP-Password && (

		# CHAP: check and done
		
		CHAP-Password == CHAP-Challenge ^
				 md5 (1 firstof CHAP-Password .
				      clear-password .
				      CHAP-Challenge) && (

			RAD-Code := Access-Accept,
			Reply-Message := "Welcome, CHAP user.\n"
		),

		halt
	),

	# Apparently neither but the users file _did_ set
	# clear-password: reject user

	halt
),


###########################################################################
#
# Step 14: Handle Md5-Hex style hashed password (PAP only) if the users
# file returned a md5-hex-password attribute. See step 14 above for more
# information about this type of password.
#
# Note that the 'REP' override is present to avoid the server searching
# for a password attribute, which it'd want to convert to numeric, negate, 
# convert to string, rewrite as hex, convert to numeric, negate, convert
# to string, and do md5 over. Heh.

REP:md5-hex-password && (

	User-Password || halt,

	32 lastof REP:md5-hex-password == 
		hex md5 (User-Password . 4 firstof REP:md5-hex-password) && (

		RAD-Code := Access-Accept,
		Reply-Message := "Welcome, Login user with md5 password.\n"
	),

	halt
),


###########################################################################
#
# Halt, rejecting the user

halt