1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
|
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
servers, and if necessary creating and maintaining connections to
the server. 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.
Summary of request and response proxied between client and server:
request from client -> (listener ->) server reader
request queue
client writer -> request to server
response from server -> client reader
reply queue
server writer -> response to 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 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 remove 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.
rq->buf is removed by freerqoutdata(), because then we will not try
to send the request in rq->buf any more.
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.
};
3. Dynamic servers
==================
A server block can contain the 'dynamicLookupCommand' option, naming
an executable file which will be executed (fork, exec) with the realm
of the user being authenticated as its only argument. The output from
the program (read from stdout) is used as configuration text and
parsed as if it was read from a configuration file.
[XXX describe what happens when config is read -- set in
createsubrealmservers() if the server block has the
'dynamicLookupCommand' option]
[XXX describe what happens when a packet is handled -- findserver()
creating a realm object]
[XXX describe the non-staticness of TLS connections to dynamically
resolved servers -- tlsclientrd and IDLE_TIMEOUT]
Dynamic freeing of resources
----------------------------
At the end of clientwr(), if server->dynamiclookuparg != NULL, the
removeserversubrealms(list, server) function is called and normally
so is freeclsrvconf(server).
removeserversubrealms() traverses the list of realms (taking a lock on
each realm) and calls _internal_removeserversubrealms(subrealm,
server) on each subrealm. If the list of subrealms is empty after
this, it's being destroyed.
The _internal_removeserversubrealms(realm list, SERVER) function
traverses the list of realms and for each realm:
- increase the refcount on the realm
- take the lock on the realm
- for srv in realm->srvconfs: if srv == SERVER: decrease ref on realm
- free all servers in realm->srvconfs matching SERVER
(list_removedata() TODO: is this function correct?)
- same thing for realm->accsrvconfs as for srvconfs
- if none of realm->srvconfs->servers and realm->accsrvonfs->servers
have dynamiclookuparg set:
- for each srv in realm->srvconfs: free srv and decrease ref on realm
- destroy realm->srvconfs
- same thing for realm->accsrvconfs as for srvconfs
- release the realm lock
- decrease the refcount on the realm
freeclsrvconf() performs 15 calls to free() and also invokes
freegconfmstr(), freehostports(), regfree() and
pthread_mutex_destroy().
|