Skip to content

Implement Asynchrounous Requests via UDP/TCP#93

Open
mriemensberger wants to merge 6 commits intoradcli:masterfrom
mriemensberger:implement-async-requests
Open

Implement Asynchrounous Requests via UDP/TCP#93
mriemensberger wants to merge 6 commits intoradcli:masterfrom
mriemensberger:implement-async-requests

Conversation

@mriemensberger
Copy link
Copy Markdown

Radius clients, for example, splash portal applications, frequently perform
lots of networking-related tasks like serving HTTP. Those applications are
often written using an event loop (based on select, poll, etc.) and non-blocking
async processing. In order to integrate radcli into such an event loop
architecture it also needs to support non-blocking async radius exchanges.

  • Implements async requests by splitting the original send_server_ctx()
    function and its wrappers into multiple pieces that are driven by a
    simple state machine and events on the associated socket. The
    rc_async_handle represents on async request, holds the state
    and the result when the request is done.
  • Stores the state for each request in a separate handle.
  • Adds async handling functions in sendserver_async.c
  • Implements parallel handling of multiple async requests in multihandle.c
    with direct support for select and poll event loop structures.

Caveats:

  • Only UDP and TCP are supported (no TLD/DTLS)
  • Automatic retries are not supported; Users can retry by creating a new
    handle and retry the request using the new handle.
  • Only one configured server is supported. rc_async_prepare_handle can
    be used to specify different servers for a different handle with
    otherwise the same request data.

Philipp Stanner and others added 4 commits February 10, 2023 02:01
Moves code needed by both sendserver.c and sendserver_async.c to a
separate file. Adds an associated header.
Radius clients, for example, splash portal applications, frequently
perform lots of networking-related tasks like serving HTTP.  Those
applications are often written using an event loop (based on select,
poll, etc.) and non-blocking async processing.  In order to integrate
radcli into such an event loop architecture it also needs to support
non-blocking async radius exchanges.

Implements async requests by splitting the original send_server_ctx()
function and its wrappers into multiple pieces that are driven by a
simple state machine and events on the associated socket.  The
rc_async_handle represents on async request, holds the state
and the result when the request is done.

Caveats:

- Only UDP and TCP are supported (no TLD/DTLS)
- Automatic retries are not supported; Users can retry by creating a new
  handle and retry the request using the new handle.
- Only one configured server is supported.  rc_async_prepare_handle can
  be used to specify different servers for a different handle with
  otherwise the same request data.
…quests

Implements parallel handling of multiple async requests in multihandle.c
with direct support for select and poll event loop structures.

This change was co-authored by Philipp Stanner.
@mriemensberger mriemensberger force-pushed the implement-async-requests branch 2 times, most recently from 44bc5c5 to 7acb752 Compare February 10, 2023 08:02
@nmav nmav self-assigned this Feb 19, 2023
Comment on lines +64 to +158
switch (vp->attribute) {
case PW_USER_PASSWORD:

/* Encrypt the password */

/* Chop off password at AUTH_PASS_LEN */
length = vp->lvalue;
if (length > AUTH_PASS_LEN)
length = AUTH_PASS_LEN;

/* Calculate the padded length */
padded_length =
(length +
(AUTH_VECTOR_LEN - 1)) & ~(AUTH_VECTOR_LEN - 1);

/* Record the attribute length */
*buf++ = padded_length + 2;
if (vsa_length_ptr != NULL)
*vsa_length_ptr += padded_length + 2;

/* Pad the password with zeros */
memset((char *)passbuf, '\0', AUTH_PASS_LEN);
memcpy((char *)passbuf, vp->strvalue, (size_t) length);

secretlen = strlen(secret);
vector = (unsigned char *)auth->vector;
for (i = 0; i < padded_length; i += AUTH_VECTOR_LEN) {
/* Calculate the MD5 digest */
strcpy((char *)md5buf, secret);
memcpy((char *)md5buf + secretlen, vector,
AUTH_VECTOR_LEN);
rc_md5_calc(buf, md5buf,
secretlen + AUTH_VECTOR_LEN);

/* Remeber the start of the digest */
vector = buf;

/* Xor the password into the MD5 digest */
for (pc = i; pc < (i + AUTH_VECTOR_LEN); pc++) {
*buf++ ^= passbuf[pc];
}
}

total_length += padded_length + 2;

break;
default:
switch (vp->type) {
case PW_TYPE_STRING:
length = vp->lvalue;
*buf++ = length + 2;
if (vsa_length_ptr != NULL)
*vsa_length_ptr += length + 2;
memcpy(buf, vp->strvalue, (size_t) length);
buf += length;
total_length += length + 2;
break;

case PW_TYPE_IPV6ADDR:
length = 16;
*buf++ = length + 2;
if (vsa_length_ptr != NULL)
*vsa_length_ptr += length + 2;
memcpy(buf, vp->strvalue, (size_t) length);
buf += length;
total_length += length + 2;
break;

case PW_TYPE_IPV6PREFIX:
length = vp->lvalue;
*buf++ = length + 2;
if (vsa_length_ptr != NULL)
*vsa_length_ptr += length + 2;
memcpy(buf, vp->strvalue, (size_t) length);
buf += length;
total_length += length + 2;
break;

case PW_TYPE_INTEGER:
case PW_TYPE_IPADDR:
case PW_TYPE_DATE:
*buf++ = sizeof(uint32_t) + 2;
if (vsa_length_ptr != NULL)
*vsa_length_ptr += sizeof(uint32_t) + 2;
lvalue = htonl(vp->lvalue);
memcpy(buf, (char *)&lvalue, sizeof(uint32_t));
buf += sizeof(uint32_t);
total_length += sizeof(uint32_t) + 2;
break;

default:
break;
}
break;
}

Check notice

Code scanning / CodeQL

Long switch case

Switch has at least one case that is too long: [PW_USER_PASSWORD (44 lines)](1).
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is code that was just moved without changes to enable reuse in the async implementation.

Comment on lines +30 to +33
/*
VALUE_PAIR *vp = NULL;
DICT_VALUE *dval = NULL;
*/

Check notice

Code scanning / CodeQL

Commented-out code

This comment appears to contain commented-out code.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed.

Comment on lines +64 to +158
switch (vp->attribute) {
case PW_USER_PASSWORD:

/* Encrypt the password */

/* Chop off password at AUTH_PASS_LEN */
length = vp->lvalue;
if (length > AUTH_PASS_LEN)
length = AUTH_PASS_LEN;

/* Calculate the padded length */
padded_length =
(length +
(AUTH_VECTOR_LEN - 1)) & ~(AUTH_VECTOR_LEN - 1);

/* Record the attribute length */
*buf++ = padded_length + 2;
if (vsa_length_ptr != NULL)
*vsa_length_ptr += padded_length + 2;

/* Pad the password with zeros */
memset((char *)passbuf, '\0', AUTH_PASS_LEN);
memcpy((char *)passbuf, vp->strvalue, (size_t) length);

secretlen = strlen(secret);
vector = (unsigned char *)auth->vector;
for (i = 0; i < padded_length; i += AUTH_VECTOR_LEN) {
/* Calculate the MD5 digest */
strcpy((char *)md5buf, secret);
memcpy((char *)md5buf + secretlen, vector,
AUTH_VECTOR_LEN);
rc_md5_calc(buf, md5buf,
secretlen + AUTH_VECTOR_LEN);

/* Remeber the start of the digest */
vector = buf;

/* Xor the password into the MD5 digest */
for (pc = i; pc < (i + AUTH_VECTOR_LEN); pc++) {
*buf++ ^= passbuf[pc];
}
}

total_length += padded_length + 2;

break;
default:
switch (vp->type) {
case PW_TYPE_STRING:
length = vp->lvalue;
*buf++ = length + 2;
if (vsa_length_ptr != NULL)
*vsa_length_ptr += length + 2;
memcpy(buf, vp->strvalue, (size_t) length);
buf += length;
total_length += length + 2;
break;

case PW_TYPE_IPV6ADDR:
length = 16;
*buf++ = length + 2;
if (vsa_length_ptr != NULL)
*vsa_length_ptr += length + 2;
memcpy(buf, vp->strvalue, (size_t) length);
buf += length;
total_length += length + 2;
break;

case PW_TYPE_IPV6PREFIX:
length = vp->lvalue;
*buf++ = length + 2;
if (vsa_length_ptr != NULL)
*vsa_length_ptr += length + 2;
memcpy(buf, vp->strvalue, (size_t) length);
buf += length;
total_length += length + 2;
break;

case PW_TYPE_INTEGER:
case PW_TYPE_IPADDR:
case PW_TYPE_DATE:
*buf++ = sizeof(uint32_t) + 2;
if (vsa_length_ptr != NULL)
*vsa_length_ptr += sizeof(uint32_t) + 2;
lvalue = htonl(vp->lvalue);
memcpy(buf, (char *)&lvalue, sizeof(uint32_t));
buf += sizeof(uint32_t);
total_length += sizeof(uint32_t) + 2;
break;

default:
break;
}
break;
}

Check notice

Code scanning / CodeQL

No trivial switch statements

This switch statement should either handle more cases, or be rewritten as an if statement.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is code that was just moved without changes to enable reuse in the async implementation.

@mriemensberger mriemensberger force-pushed the implement-async-requests branch from 7acb752 to 27fd8ba Compare March 13, 2023 08:24
University of Michigan and Merit Network, Inc. shall not be liable for any
special, indirect, incidental or consequential damages with respect to any
claim by Licensee or any third party arising from use of the software.
------------------------------------------------------------------------------
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please no new license. Just add the Copyright at the beginning of the file.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to clarify: After line 8 in this file, right? Will do.

@nmav
Copy link
Copy Markdown
Contributor

nmav commented Jan 8, 2024

Sorry for getting to it with such a big delay. 2023 was a crazy year for me. I like the idea, and I would like to review. At the same time there are not any functional tests and for introducing a new API, tests are necessary to ensure that it works as expected, and that future changes do not modify this functionality. If I get to review are you still interested to enhance it with functional tests?

@mriemensberger
Copy link
Copy Markdown
Author

Sorry for getting to it with such a big delay. 2023 was a crazy year for me. I like the idea, and I would like to review. At the same time there are not any functional tests and for introducing a new API, tests are necessary to ensure that it works as expected, and that future changes do not modify this functionality. If I get to review are you still interested to enhance it with functional tests?

No worries. I have a small amount of time for upstreaming the changes.

Regarding the functional tests. There is the radembedded_async.c, which exercises the proposed async api similar to radembedded.c. Does it just need an additional wrapper in tests? Or are you looking for more tests beyond what is in radembedded_async.c?

/*
* Copyright (C) 2022 Cadami GmbH [email protected]
*
* radembedded.c - a sample c program showing how to embed the configuration of a radius
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/rademedded.c/radembedded_async.c

@nmav
Copy link
Copy Markdown
Contributor

nmav commented Jan 9, 2024

Sorry for getting to it with such a big delay. 2023 was a crazy year for me. I like the idea, and I would like to review. At the same time there are not any functional tests and for introducing a new API, tests are necessary to ensure that it works as expected, and that future changes do not modify this functionality. If I get to review are you still interested to enhance it with functional tests?

No worries. I have a small amount of time for upstreaming the changes.

Regarding the functional tests. There is the radembedded_async.c, which exercises the proposed async api similar to radembedded.c. Does it just need an additional wrapper in tests? Or are you looking for more tests beyond what is in radembedded_async.c?

To run the new code during CI and validate the expected output, for example as in tests/radembedded-tests.sh

@nmav
Copy link
Copy Markdown
Contributor

nmav commented Jan 9, 2024

I see that no github workflows were run in this PR.

@mriemensberger
Copy link
Copy Markdown
Author

mriemensberger commented Jan 9, 2024

Regarding the functional tests. There is the radembedded_async.c, which exercises the proposed async api similar to radembedded.c. Does it just need an additional wrapper in tests? Or are you looking for more tests beyond what is in radembedded_async.c?

To run the new code during CI and validate the expected output, for example as in tests/radembedded-tests.sh

Sure. I'll add a similar wrapper for the async version of those tests.

@nmav
Copy link
Copy Markdown
Contributor

nmav commented Jan 9, 2024

I see that no github workflows were run in this PR.

That is a configuration issue of github actions. I've tried to fix it in master, please try rebasing.

* Copyright 1992,1993, 1994,1995 The Regents of the University of Michigan
* and Merit Network, Inc. All Rights Reserved
*
* Copyright (C) 2022 Cadami GmbH, [email protected]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seem to be on a strange position. I'd expect it to be on the top together with the BSD2 boilerplate.

exit(EXIT_FAILURE);
}

/* ======================== Prepare New Request ============================= */
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems the function is too long. Should we split into different functions for easier readbility?

};


struct rc_multihandle {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not clear to me what is the intention of the multihandle. Some documentation is needed here to explain why this exists and what it does.


for (; received_responses < count;) {
/* Get all the currently enqueued pollable file descriptors. */
nr_of_poll_fds = rc_multi_get_pollfds(mhdl, polli, count);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is my concern is that the API is very poll-system-call-centric. Today common ways to do it is also with epoll, kqueue or libev. How does the API accomodate these?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is also rc_multi_get_fd_set and rc_multi_process_fd_set to be used with select.

Also, the entire multihandle API is a thin layer on top of the rc_async_* API to provide an easy way to handle multiple radius requests with poll or select. Those syscalls always report the entire set and multihandle helps with walking and processing the pollfds array or select fdset. It's loosely inspired by libcurl but much simpler.

rc_async_* can also directly be used with and event multiplexer syscall as it exposes the associated fd and the expected events in any state.

Extending the multihandle API to more complex event multiplexer syscalls like epoll and kqueue might be possible although I'm not sure how useful that would be given those syscalls report events not for the entire set of descriptors but only for descriptors that saw events.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, we should not provide wrappers for all possibilities, just make sure that it is possible to use the API with them. Documentation seem to be important towards that, so that the async API is not perceived as poll-only.

@nmav
Copy link
Copy Markdown
Contributor

nmav commented Jan 24, 2024

Taking a note for me of what remains for final review:

  • Rebase to master
  • Documentation that covers usage with epoll / libev
  • Documentation for structures such as multihandle
  • Tests that run on CI

@mriemensberger
Copy link
Copy Markdown
Author

Thanks @nmav!

It will however take some time until I find the time to go through those points and address them.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants