diff options
-rw-r--r-- | develdoc.txt | 281 |
1 files changed, 281 insertions, 0 deletions
diff --git a/develdoc.txt b/develdoc.txt new file mode 100644 index 0000000..d3ee6f7 --- /dev/null +++ b/develdoc.txt @@ -0,0 +1,281 @@ +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. + +}; |