Commit 78df329e authored by venaas's avatar venaas Committed by venaas
Browse files

first revision of developers doc for radsecproxy

git-svn-id: https://svn.testnett.uninett.no/radsecproxy/trunk@438 e88ac4ed-0b26-0410-9574-a7f39faa03bf
parent e8f6c5b3
radsecproxy documentation for developers
1. Overall design
At startup client and server configurations are read. Two lists
are created, called clconfs and srvconfs. Both contain clsrvconf
structs.
For each server config, a client writer thread is created. This
takes care of sending requests to a server.
Next for each known transport type which has a configured client,
we create listeners. Typically there is a default server port
that will be listened on, but multiple ports might be configured.
For each port there will normally be 1-2 sockets (IPv4 and/or IPv6).
For each socket a thread is created with the listener() defined for
the transport.
This is all that happens in the main thread. The threads created
above need to take care of the rest.
Client writers are generally responsible for sending messages to
clients, and if necessary creating and maintaining connections to
the client. Client writers create threads for handling replies from
servers. If connections are used, one thread is created for reading
from each connection. clientwr() will use connecter() and
clientconnreader() definitions for the transport.
The listeners may receive RADIUS messages directly, which is the
case for UDP which is not connection based. Or they may receive
connections and create a new thread for handling each incoming
connection, where that thread will receive RADIUS messages.
The function receiving RADIUS client requests is generally called
xxxserverrd, where xxx is the transport name. The server reader
is responsible for creating a server writer thread that takes care
of sending RADIUS replies to a client.
2. RADIUS message processing
In 1 we described the threads used and the high level operations.
We will now describe how RADIUS messages are processed and flow
through the system.
An incoming RADIUS request from a client is handled by a server
reader. The server reader calls newrequest() to obtain a request
struct. It sets rq->buf to point to the received message, rq->from
to point to the client struct for the client, and might for some
transports specify additional data. E.g. for UDP, the source port
and socket it was received on. The reader must make sure buf is
at least as large as the specified RADIUS message length. The
client reader calls radsrv(rq). When that returns it just waits
for the next message.
radsrv() is in a way the core part of the proxy. It takes care
of validation, processing and routing of incoming requests.
It first creates a radmsg struct calling buf2radmsg(). This also
takes care of basic validation (e.g. that lengths add up) and
checking message authenticators. Unless it receives a valid
Access Request, Accounting Request or Status Server, it will
drop the request and return.
It next calls addclientrq() which adds this request to a request
queue for rq->from. Before adding it to the queue, it checks if
it is a duplicate of something already in the queue. If a
duplicate, radsrv() drops the request and returns.
Next radsrv() checks if it received a Status Server message. In
that case it responds with Access Accept and returns.
Next it applies any rewritein rules and also checks TTL attribute.
If TTL expired, it will drop request and return.
Next it looks for a User-Name attribute. If not present it will
will drop the request. However, if it is an accounting request
it will first send an accounting response.
Next it calls findserver() to pick a server for sending the
message to. For this it will use the user-name attribute and the
realm definitions. It also takes into account which servers are
alive.
If no server is found it will drop the message. However, in
certain cases it may send a reject or accounting response message.
Next it reencrypts any attributes that are encrypted based on
the secrets of clients/servers. And after that, decrements TTL if
present, and applies any rewriteout rules.
Finally radsrv() calls sendrq(rq) to pass the request to the
chosen server.
sendrq() checks the request queue for a server. The request queue
is an array holding 256 entries, one for each possible message ID.
Normally it will start looking for a free slot at the ID after the
last entry inserted in the queue (0 follows 255). However in a
special case where sendrq() is called to send a status-server message,
it will always use ID 0. If status-server is enabled, ID 0 is not used
for other requests. If there are no free slots, the message is
discarded.
When finding a free slot, it does, "to->requests[i].rq = rq" and
signals the writer thread : "pthread_cond_signal(&to->newrq_cond)".
After that, it returns, and the server reader thread can wait for a
new client request.
We will now consider the client writer thread that takes care of
sending this request to a server.
clientwr() continually looks for requests in its request buffer
and tries to send them to a server. It uses timers so that it can
sleep waiting for a new request, or sending status server, or
re-sending an existing request. When a new request comes in, it
will send it ASAP to the server and set tries to 1. For the
server there is a retryinterval timer. retryinterval seconds later
clientwr() will resend or remove the request. It is removed if the
server's retrycount parameter is exceeded (0 retries if reliable
transport). Status server messages are never resent.
The handling of the request stops here, unless we get a reply.
We will now describe how replies are handled.
Each transport has a function called something xxxclientrd() for
receiving replies from a server. This is run as a separate thread.
All they do is read a RADIUS message and call replyh(server, buf)
where server points to the server struct for the server, and buf
is a buffer large enough to contain the entire RADIUS message. It
will not read another message until replyh() returns.
We will now consider replyh(). It will first check if there is an
outstanding request matching the id of the reply. This is done by
checking the request queue of the server.
If it maches a request, it will validate and authenticate the
reply by calling buf2radmsg(). If this fails or the message type
is not one of Access Accept, Access Reject, Access Challenge or
Accounting Response, the reply is ignored.
If the request was a status-server message, it is simply removes
the request and returns.
Next it will apply any rewritein rules and check TTL attribute if
present. If TTL is exceeded, the reply is ignored.
Next it reencrypts some attributes with the secret of the client
the original request came from, and which the reply will be sent
back to. It also applies any rewriteout rules.
Finally to pass the reply back to the client, it does
"rqout->rq->msg = msg" to store the reply message in the request,
and calls sendreply() with rqout->rq as parameter. When
sendreply() returns, we free the request from the server's
request queue. This also means that the ID can be used for a new
request.
Now about sendreply(). All it does, is basically to assemble the
reply message, take care of authenticators and set rq->replybuf
to point to the result. After that it adds a pointer to rq to
the clients reply queue, and signals the server writer who is
responsible for sending replies to the client.
The server writer is a separate thread created by the server reader,
typically called something like xxxserverwr. All it does is to send
whatever it finds in its replyq to the client and removes it. When
the queue is empty it waits for a signal from a sendreply().
The above shows the complete flow. It might be worth also looking a
bit more at the state created for each request though.
As mentioned above, each request received from a client is stored in
request queue for the client. The request is stored in a request
struct which looks like this:
struct request {
uint8_t *buf, *replybuf;
struct radmsg *msg;
...
};
This request will remain in the queue until a new request is
received with the same id and which is not a duplicate. The
new one then replaces the previous.
Initially for a new request, only buf is used (of the above specified
fields). Next the message is parsed and validated, and if ok, it is
stored in msg. buf with the request is freed.
In sendrq() a request that is to be sent to a server, is again
reassembled from rq->msg into rq->buf.
When a reply is received, it will again be parsed and validated, and
if ok, it will free the old rq->msg, and store the new instead.
Finally, in sendreply() rq->replybuf is created from rq->msg, and
rq->msg is freed. rq->replybuf is kept so that if a duplicate request
is received later, we can just return rq->replybuf.
It appears that rq->buf created by sendrq() is stored until the
request is replaced, it should be freed when replyh replaces rq->msg
or in freerqoutdata().
Request structs should perhaps be freed when they "expire", rather
than wait until a new request with the same ID comes along.
x. Transports
struct protodefs protodefs[] contains definitions of the different
transport protocols. We will here describe the different parameters.
struct protodefs {
char *name;
This should be a textual name for the transport, e.g. "udp". This is
used in client/server configurations and for debug/log messages.
char *secretdefault;
Some transports like TLS that provides strong encryption, may have a
default RADIUS secret, since the RADIUS encryption is not needed.
uint8_t socktype;
Typically set to SOCK_DGRAM or SOCK_STREAM. This is used when a
socket for the transport is created.
char *portdefault;
The default server port for the transport, e.g. 1812.
uint8_t retrycountdefault;
How many time a client request should be resent. For a reliable
transport like TCP/TLS, this should be 0.
uint8_t retrycountmax;
The maximum allowed configurable value for retrycount. For reliable
transport it should probably be 0.
uint8_t retryintervaldefault;
This is the default for how many seconds there should be between each
retry. For a reliable transport with 0 retries, this controls how
long it should wait for a reply to the client request.
uint8_t retryintervalmax;
This is the maximum allowed retryinterval
uint8_t duplicateintervaldefault;
This is the time period two requests with the same UDP source port
and request authenticator are considered duplicates. If a reply has
been sent to the first request, then that is resent. If no reply
has been sent, the second request is ignored.
void *(*listener)(void*);
Each transport must define a listener function for the sockets
created by the transport. The socket is created by radsecproxy
core. If successful, the listener is called.
int (*connecter)(struct server *, struct timeval *, int, char *);
When creating a new server, a clientwr() thread is created for sending
requests to the server. If a connecter() is defined for the transport,
clientwr() will call connecter() and exit if connecter() returns 0.
void *(*clientconnreader)(void*);
If a connecter() is defined, then when that successfully returns,
a separate thread is created for reading from the connection. This
thread is responsible for handling replies from a server.
int (*clientradput)(struct server *, unsigned char *);
Used by clientwr() for sending a RADIUS message.
void (*addclient)(struct client *);
Need only be defined if need to override default client creation.
Used by UDP to have a common reply queue, rather than one per client.
void (*addserverextra)(struct clsrvconf *);
Need only be defined if something needs to be done in addition to
the default server creation.
uint8_t freesrcprotores;
Whether should free the resolver state for source ports and
addresses after initial startup.
void (*initextra)();
Can be defined it extra initialisation is needed for the transport.
};
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment