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. (listener) > server reader <-- requests from clients client writer --> requests to a server (client writer) > client reader <-- responses from a server (server reader) > server writer --> responses to a client TODO: they are all their own threads? Legend: > creates --> sends message <-- receives message 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 the realm->srvconfs nor the realm->accsrvonfs has a dynamiclookupcommand: - 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().