Interpreter
The Unlang interpreter is a simple policy language which has a small set of pre-defined Keywords. It supports if/then/else Conditional expressions, and dynamic ref:xlat/index.adoc[String expansions]. It has a small number of predefined data types.
Limitations
The unlang
interpreter allows adminstrators to write rules which do
the following:
-
look at packet contents, via predefined field names which are represented as attributes,
-
define both simple and complex policies based on the attributes,
-
interact with databases, usually via pre-defined behaviors (e.g. "allocate IP from pool"),
-
update the contents of replies, using a clear and simple syntax.
The language is not intended to be a general purpose programming language. It does not permit infinite loops, local variables, functions, etc.
Despite these limitations, it has proven to be useful and powerful.
While the server includes plugins for languages such as
lua,
python, and
perl, most policies can be done
in simple unlang
statements. More general purpose programming
languages are more powerful than unlang
, but they are generally
sustantially slower.
Programming Model
The unlang
programming model is simple. The server receives a
packet (i.e. request) from the network, runs it through a series of
policies, and then sends a reply. Each stage of this process is
called out in the configuration files as a separate section. For
example:
recv foo {
if (!ok) {
fail
}
}
The processing sections are defined in
virtual servers. The
virtual server documentation
describes the larger context of which packets are recieved, and when
they are received. This section concentrates on the more narrow topic
unlang
itself.
Receiving Requests
When the server recieves a packet, the data is decoded and placed as
attributes into the request
list. The packet is then run through a
processing section of a virtual server, which is typically named for
the packet type. For example, a RADIUS virtual server would process
Access-Request
packets through the recv Access-Request
section.
The result of processing the packet is a set of attributes in the
reply
list. Those attributes are encoded, and sent in a reply
packet.
Processing Requests
Each processing section is a list of modules to execute or Unlang statements to process. The list is processed in order from top to bottom.
In some cases, it is useful to skip parts of the list or to return
early from processing a list. For example, if a reject
is sent
because of a policy rule, there is usually no reason to continue to
process that list.
One key difference between normal programming languages and Unlang is the concept of modules. The modules are treated as keywords in the language which contain complex behavior. For example, the sql module has a complex set of pre-defined behavior, depending on what kind of packet it is processing.
The ability to place complex logic into a module means that common
functions such as "allocate IP from SQL" is little more than a
one-line statement in unlang
:
recv Access-Request {
sqlippool
if (!ok) {
reject
}
}
The benefit of this approach is that the policies are simple: "allocate an IP, if that doesn’t work, send a reject". However, the details of those policies are hidden behind the sqlippool configuration. This abstraction allows the underyling sqlippool configuration to be changed from MySQL to PostgreSQL without affecting any of the policies.
The server includes dozens of modules, each of which defines a complex set of behavior. Please see the modules documentation for more information.
Algorithm
The interpreter starts off each request with a default return code and priority. It processes the request through a processing section.
Each statement or module is evaluated, and the statement return code is used to update the final return code associated with the request. When the list is finished, the server either continues to the next section or sends a reply to the request.
When a module is executed, it returns a code
to the interpreter. This code tells the interpreter if the module was
ok
, or if it did nothing (noop
), or if the module failed (fail
).
The table below shows the names of the return codes and their
meanings.
Return code | Description |
---|---|
|
The operation failed. Usually as a result of an external dependency like a database being unavailable or an internal error. |
|
The request has been "handled", no further policies in the current section should be called, and the section should immediately exit. |
|
The request, or operation, was invalid. In the case of requests this usually indicates absent or malformed attribute values. |
|
The operation did nothing. |
|
A 'lookup' operation returned no results. |
|
Operation completed successfully but did not change any attributes in the request. |
|
The operation indicates the current request should be 'rejected'. What this actually means is different from protocol to protocol. It usually means that access to the requested resource should be denied, or that the current request should be NAKd. Usually returned when provided credentials were invalid. |
|
The operation completed successfully and updated one or more attributes in the request. |
|
Access to a particular resource is
denied. This is similar to |
|
Returned by an operation when execution of a request should be suspended. |
In versions ≤ v3.2.x the |
The "return code" used by unlang
is somewhat similar to return codes
used by functions in other languages. However, this return code is
built into unlang
, because it simplifies most common scenarios. For
example, automatic load-balancing
would be much more difficult without unlang
hiding much of the
underlying complexity.
The result is that the interpreter simply runs a series of statements and modules. Depending on what is returned, the interpreter either stops, or continues.
The algorithm used by the interpreter is given in the following pseudo-code:
(code, 0) = action_table[default]
foreach (statement in section) {
code' = evaluate(statement)
(action, priority') = action_table[code']
if (action == return) {
code = code'
break;
}
if (priority' >= priority) {
(code, priority) = (code', priority')
}
}
return (code, priority)