Skip to content

Allow white spaces (spaces and tabs) in shared secret by the use of a quoted string (addresses issue #105), add TCP support for specified servers (starting with "tcp://"), and added RADIUS ovre TLS (RADSEC) support for servers starting with "tls://"#110

Open
raphpen wants to merge 17 commits into
FreeRADIUS:masterfrom
raphpen:master

Conversation

@raphpen

@raphpen raphpen commented Feb 17, 2025

Copy link
Copy Markdown

Allow white spaces (spaces and tabs) in shared secret by the use of a quoted string, and add size checks on src_ip and vrf.

If the secret begins with a single quote "'" treat it has a quoted string an read
up to the ending quote. If any other single quotes are present they must be escaped
with a backslash ("\").
Eg. ' secret with spaces \' quotes and  tabs'

If the secret value starts with a single quote ("'") and has no spaces, you may
just escape it with a blackslash ("\").
E.g. \'secret_starts_with_quote_but_is_not_enclosed

If the secret value starts with "\'", just add an extra backslash.
E.g. \\'secret_starts_with_backslash_single_quote.....

Without _PARSE_STRICT, as before, parsing ignores non numerical timeouts and any extra trailing
content (words) after the vfr field.

By defining _PARSE_STRICT such bad timeouts or extra content raise error.

Raphael Pennisi and others added 5 commits February 10, 2025 14:40
… quoted string, and add size checks on src_ip and vrf.

If the secret begins with a single quote "'" treat it has a quoted string an read
up to the ending quote. If any other single quotes are present they must be escaped
with a backslash ("\").
Eg. ' secret with spaces \' quotes and 	tabs'

If the secret value starts with a single quote ("'") and has no spaces, you may
just escape it with a blackslash ("\").
E.g. \'secret_starts_with_quote_but_is_not_enclosed

Without _PARSE_STRICT, as before, parsing ignores non numerical timeouts and any extra trailing
content (words) after the vfr field.

By defining _PARSE_STRICT such bad timeouts or extra content raise error.
@raphpen

raphpen commented Feb 24, 2025

Copy link
Copy Markdown
Author

As mentioned on issue #102 I also added support for TCP, if the server name or address begins with "tcp://", TCP connection is used.

@raphpen raphpen changed the title Allow white spaces (spaces and tabs) in shared secret by the use of a quoted string (addresses issue #105) Allow white spaces (spaces and tabs) in shared secret by the use of a quoted string (addresses issue #105) and add TCP Support for specified servers (begins with "tcp://") Feb 24, 2025
raphpen and others added 3 commits February 24, 2025 09:59
WARNING: Openssl library is used and required.

Basically to use it provide required client certificate, and optionally the authenticating CAs, by adding the following parameters to the pam config line (both auth and session):

cert = absolute pathname of the client certificate file (PEM format)

key = absolute pathname of the client private key file

ca = absolute pathname to the know and authentication CAs file

Then specify RADSEC usage by adding the prefix "tls://" to the desired server address or hostname in the pam_radius_auth.conf file.

By default servers certificates are verified, you may ignore failures by adding the "verify=no" option.

You may force RADSEC usage on all servers, without the "tls://" prefix, by setting "radsec=yes".

You may disable RADSEC usage, falling back all "tls://" to RADIUS over UDP, by setting "radsec=no".

By default, with "radsec=try" , if SSL  setup works, RADSEC is used for "tls://", otherwise they fallback to RADIUS over UDP.

WARNING: SELinux may deny proper functioning.
For testing propouse disable it with:

  setenforce 0

In production environment add, for example, the following rules to authorize usage of 1812/tcp, 1813/tcp and 2083/tcp by sshd:

require {
	type radius_port_t;
	type radsec_port_t;
	type sshd_t;
	type radacct_port_t;
	class tcp_socket name_connect;
}

allow sshd_t radacct_port_t:tcp_socket name_connect;
allow sshd_t radius_port_t:tcp_socket name_connect;
allow sshd_t radsec_port_t:tcp_socket name_connect;
@raphpen

raphpen commented Mar 4, 2025

Copy link
Copy Markdown
Author

Hi @alandekok ,
If still interested, I have also added RADIUS over TLS (RADSEC) support.

Build artifacts (configure and make, macro HAVE_OPENSSL) are still missing. But I will add them if needed.

@alandekok

Copy link
Copy Markdown
Member

This looks very interesting, thanks!

I'm pretty much out until the end of March (traveling / conferences) but I will take a look when I get back. I think these patches are very useful.

@jake-scott jake-scott left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hi - its funny how we both submitted TCP support more or less at the same time!

Your changes are certainly simpler than mine. A few notes, plus I think we need separate timeouts for connect vs. reading a response -- usually you only want to wait a second or two max for a connect before failing over to a working server but you might want to wait much longer for a response especially if there is an out of band auth going on for example.

Comment thread src/pam_radius_auth.c

/* open a socket. Dies if it fails */
*sockfd = socket(AF_INET, SOCK_DGRAM, 0);
*sockfd = socket(AF_INET, (tcp? SOCK_STREAM: SOCK_DGRAM), 0);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The issue with initializing the TCP socket here is that you can't re-use a TCP socket once you have tried to connect it. You have to close it and re-create with a new socket().

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.

Checkout initialize() in src/pam_radius_auth.c at line 1028:

/* TCP needs its own socket */
if (valid_src_ip == 0 || vrf[0] || server->tcp) {
#ifndef NDEBUG
if(server->tcp) _pam_log(LOG_DEBUG,"Use TCP for %s\n",server->hostname);
#endif
if (initialize_sockets(conf, &server->sockfd, &server->sockfd6, &salocal4, &salocal6, vrf, server->tcp) != 0) {

For each TCP server a dedicated socket is used.

Compile and try it your self with various faulty servers.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

ah I'm sorry I didn't notice you were stuffing the TCP socket into the server struct and not storing it in the global context..

Comment thread src/pam_radius_auth.c
}

/* set up the local end of the socket communications */
if (bind(*sockfd, (struct sockaddr *)salocal4, sizeof (struct sockaddr_in)) < 0) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

It is valid to bind TCP sockets to source addresses as it is for UDP.

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.

Checkout at line:

/* If not TCP or has a source address set up the local end of the socket communications /
if(!tcp || ((struct sockaddr_in)salocal4)->sin_addr.s_addr != 0
) {
if (bind(*sockfd, (struct sockaddr *)salocal4, sizeof (struct sockaddr_in)) < 0) {
char error_string[BUFFER_SIZE];
get_error_string(errno, error_string, sizeof(error_string));
_pam_log(LOG_ERR, "Failed binding to port: %s", error_string);
return -1;
}
}

If a source address is specified I do bind the TCP socket, otherwise I let do it at connect().

Comment thread src/pam_radius_auth.c Outdated

while(1) {
#ifdef HAVE_POLL_H
rcode = poll((struct pollfd *) &pollfds, 1, tv.tv_sec * 1000);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

It is likely that tv_usec won't be zero in anything except the first loop. Maybe add tv_sec * 1000 here.

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.

Basically the problem is that poll(), unlike select() (and ppoll() that is less portable), does not updates the timeout.

So I borrowed the code from the EINTR case of the recvfrom loop already in use.

As time() works on seconds, while manually updating the timeout, we may underestimate or overestimate. In real cases we may wait a little more (<1 sec) than the specified timeout.

More precise time keeping may be done using clock_gettime();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

right - you have to get the current time of day -- I used gettimeofday() - take a look at : https://github.com/jake-scott/pam_radius/blob/jake/tcp-support/src/pam_radius_auth.c#L1040 if that's helpful.

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.

Thanks @jake-scott for the nice suggestion.

I will change the timeout updating code with gettimeofday(). Just give me some days,
I'm temporarily busy with my usual corporate IT job.

By the way, I think it would be very nice (mostly for TCP and TLS), make the most of poll() and select() multiplexing, attempting to establish a connection to the servers in parallel, and sending the request (Access or Accounting) to the fastest.

Already done very similar stuff in the context of mass concurrent SNMP polling and ICMP monitoring (for the aforementioned job).

Hope to be back soon with more code updates.

Comment thread src/pam_radius_auth.c Outdated

tv.tv_sec = end - now;
if (tv.tv_sec == 0) { /* keep waiting */
tv.tv_sec = 1;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I think its valid to wait < 1second?..

Comment thread src/pam_radius_auth.c Outdated
total_length = 0;
while (ok) {
#ifdef HAVE_POLL_H
rcode = poll((struct pollfd *) &pollfds, 1, tv.tv_sec * 1000);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Again I don't think we have to wait for exact multiples of one second

Comment thread src/pam_radius_auth.c Outdated
} else {
tv.tv_sec = end - now;
if (tv.tv_sec == 0) { /* keep waiting */
tv.tv_sec = 1;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

ditto..

Comment thread src/pam_radius_auth.c Outdated
/* If TCP we must connect */
if(server->tcp) {
if(conf->debug) _pam_log(LOG_DEBUG,"Setup connection for %s\n",server->hostname);
rcode = connect_tmout(sockfd, server->ip, server->ip->sa_family == AF_INET? sizeof(struct sockaddr_in): sizeof(struct sockaddr_in6), server->timeout);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This won't work if sockfd has been used before like if this is not the first attempt of the first TCP server.

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.

I know.
Each TCP server has its own dedicated socket.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

thanks..

@jake-scott jake-scott mentioned this pull request Mar 4, 2025
@raphpen

raphpen commented Mar 5, 2025

Copy link
Copy Markdown
Author

Hi @jake-scott,
thanks for your comments.

Actually, having some spare time, I was looking at the issues, and working on what seem interesting for me. So sorry.

You are right, having separate connect and read timeout for TCP make absolutely. I will added soon.

Now let me respond to your other questions.

Use ./configure --disable-radsec to compile without openssl and RADSEC.

Use ./configure --enable-radsec to force support for RADSEC, if openssl is not usable configuration fails.

By default ./configure checks openssl usability and if present enables RADSEC support.

WARNING. My autoconf is older (2.69) then the last ìn use (2.71)
@raphpen raphpen changed the title Allow white spaces (spaces and tabs) in shared secret by the use of a quoted string (addresses issue #105) and add TCP Support for specified servers (begins with "tcp://") Allow white spaces (spaces and tabs) in shared secret by the use of a quoted string (addresses issue #105), add TCP support for specified servers (starting with "tcp://"), and added RADIUS ovre TLS (RADSEC) support for servers starting with "tls://" Mar 5, 2025
Now the timeout field (in pam_radius_auth.conf) accepts the following syntax:

read_timeout[,connect_timeout]

This means you may additionally specify a connection timeout,
by default it is set to 1 second, and it is used only for TCP or
TLS (RADSEC) servers.
@raphpen

raphpen commented Mar 5, 2025

Copy link
Copy Markdown
Author

Now the timeout field (in pam_radius_auth.conf) accepts the following syntax:

read_timeout[,connect_timeout]

This means you may additionally specify a connection timeout,
by default it is set to 1 second, and it is used only for TCP or
TLS (RADSEC) servers.

Raphael Pennisi added 2 commits March 6, 2025 09:57
In the most general case the radius protocol exchange may
require more then a single request-response (eg: Access-Challenge).

So we must avoid reconnectiong an already connected socket,
and keep track of the last usable SSL context.
Comment thread src/pam_radius_auth.c Outdated
tv.tv_sec = end - now;
if (tv.tv_sec == 0) { /* keep waiting */
tv.tv_sec = 1;
timersub(&end,&now,&tv);

@jake-scott jake-scott Mar 10, 2025

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Wasn't aware these were included in glibc. That's nice -- is it there on all the platforms supported by this code though? (is Solaris still supported?)

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.

Maybe not, but it's an easy macro from /usr/include/sys/time.h:

define timersub(a, b, result) \

do {
(result)->tv_sec = (a)->tv_sec - (b)->tv_sec;
(result)->tv_usec = (a)->tv_usec - (b)->tv_usec;
if ((result)->tv_usec < 0) {
--(result)->tv_sec;
(result)->tv_usec += 1000000;
}
} while (0)

If missing we may add such definition.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Ah OK good news. Reading the glibc page that I got the timeval_subtract function from again, that extra complexity is to deal with systems where tv_sec is unsigned. I'm not sure which those are! (https://ftp.gnu.org/old-gnu/Manuals/glibc-2.2.3/html_node/libc_418.html). Solaris/Linux/BSD have signed members thankfully.

If the client private key is encrypted you may specify the encryption
password by adding the "key_password=" parameter to the pam config file.
@jake-scott

Copy link
Copy Markdown

Hi @alandekok did you have any thoughts about this PR?

@alandekok

Copy link
Copy Markdown
Member

I'll take a look at it next week.

resource.

At first, if any, all TCP and TLS servers are tried together.

The first one ready, connected and handshaked, is used. If something
goes wrong the others get used, as they are ready.

When no more TCP or TLS server are usable, UDP are tried one by one in the config order.
@raphpen

raphpen commented Apr 22, 2026

Copy link
Copy Markdown
Author

Hello,
any chance to get merged?

I have improved the code. Now all TCP/TLS servers are tried first and together, so that we can use the first one that is ready.

@alandekok

Copy link
Copy Markdown
Member

thanks. I'll review this in the next week or two, and pull it in.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants