Compare commits

..

No commits in common. "master" and "master" have entirely different histories.

6 changed files with 88 additions and 146 deletions

View file

@ -1,17 +1,11 @@
CFLAGS = -flto -Wall -Wextra -Werror -Wl,-z,now
CFLAGS_RELEASE = ${CFLAGS} -O3 -s -D_FORTIFY_SOURCE=2
CFLAGS = -Wall -Wextra -Werror -Wl,-z,now
CFLAGS_RELEASE = ${CFLAGS} -O2 -s -D_FORTIFY_SOURCE=2
CFLAGS_DEBUG = ${CFLAGS} -O0 -g -fsanitize=undefined
CFLAGS_STATIC = ${CFLAGS_RELEASE} -static-pie
LIBS = -lcrypt
CC = cc
CC = gcc
PREFIX ?= /usr/local
BINDIR = ${PREFIX}/bin
MANDIR = ${PREFIX}/share/man
all: rdo
rdo: rdo.c
all: rdo.c
${CC} ${CFLAGS_RELEASE} rdo.c -o rdo ${LIBS}
static: rdo.c
@ -20,29 +14,17 @@ static: rdo.c
debug: rdo.c
${CC} ${CFLAGS_DEBUG} rdo.c -o rdo ${LIBS}
install: all
mkdir -p ${DESTDIR}${BINDIR}
cp rdo ${DESTDIR}${BINDIR}/rdo
chmod 4755 ${DESTDIR}${BINDIR}/rdo
mkdir -p ${DESTDIR}${MANDIR}/man1
cp rdo.1 ${DESTDIR}${MANDIR}/man1/rdo.1
chmod 644 ${DESTDIR}${MANDIR}/man1/rdo.1
mkdir -p ${DESTDIR}${MANDIR}/man5
cp rdo.conf.5 ${DESTDIR}${MANDIR}/man5/rdo.conf.5
chmod 644 ${DESTDIR}${MANDIR}/man5/rdo.conf.5
mkdir -p ${DESTDIR}/etc
@if [ -f ${DESTDIR}/etc/rdo.conf ]; then \
echo "Skipping existing configuration file: ${DESTDIR}/etc/rdo.conf"; \
else \
cp rdo_sample.conf ${DESTDIR}/etc/rdo.conf; \
chmod 644 ${DESTDIR}/etc/rdo.conf; \
fi
install: rdo
cp rdo ${DESTDIR}/usr/local/bin/rdo
chown 0:0 ${DESTDIR}/usr/local/bin/rdo
chmod 755 ${DESTDIR}/usr/local/bin/rdo
chmod u+s ${DESTDIR}/usr/local/bin/rdo
cp rdo_sample.conf ${DESTDIR}/etc/rdo.conf
chmod 600 ${DESTDIR}/etc/rdo.conf
uninstall:
rm -f ${DESTDIR}${BINDIR}/rdo
rm -f ${DESTDIR}${MANDIR}/man1/rdo.1
rm -f ${DESTDIR}${MANDIR}/man5/rdo.conf.5
rm /usr/local/bin/rdo
rm /etc/rdo.conf
clean:
rm rdo

View file

@ -1,16 +1,18 @@
# RootDO
# RootDO [![AUR](https://img.shields.io/aur/version/rdo.svg)](https://aur.archlinux.org/packages/rdo/)
This project aims to be a very slim alternative to both sudo and doas.
### Installation
If you are on Arch Linux, you can download the package via the [AUR](https://aur.archlinux.org/packages/rdo/).
You can clone and build rdo with the following set of commands:
```sh
git clone https://mrrp.sx7n8.tech/soccera/rdo.git
git clone https://codeberg.org/sw1tchbl4d3/rdo
cd rdo
make
rdo make install
sudo make install
```
After that, you'll have to configure rdo to allow you to use it.
@ -20,7 +22,7 @@ Then you're good to go!
To uninstall:
```sh
rdo make uninstall
sudo make uninstall
```
### Usage
@ -35,8 +37,6 @@ Or, to get the password from stdin:
rdo - [command]
```
`rdo` will ask for your password and grant you a session, on successful authentication you will be able to use `rdo` for the time specified in `session_ttl`. You have 3 attempts to enter the correct password.
The configuration file has the following variables:
```
group=wheel
@ -45,9 +45,34 @@ session_ttl=5
```
- `group`: The group of users that is allowed to execute rdo.
- `wrong_pw_sleep`: The amount of milliseconds to sleep at a wrong password attempt. Must be a positive integer. Set to 0 to disable. Defaults to 1000.
- `session_ttl`: The amount of minutes a session lasts. Must be a positive integer. Set to 0 to disable. Defaults to 5.
- `wrong_pw_sleep`: The amount of milliseconds to sleep at a wrong password attempt. Must be a positive integer. Set to 0 to disable.
- `session_ttl`: The amount of minutes a session lasts. Must be a positive integer. Set to 0 to disable.
### License
### Benchmarks
This project is licensed under the GNU General Public License v3.0. See the `LICENSE` file for the full license text.
The benchmark: Execute `whoami` (GNU coreutils 9.1) 10000 times.
Yes, this is a silly benchmark. Yes, the performance gain in real world application is close to nothing.
But it's fun!
|Program|Time|
--- | ---
sudo 1.19.11 | 46.85s
doas 6.8.2 | 32.57s
rdo 1.4.2 | 13.37s
Baseline | 7.95s
> Baseline here is how long it took without any wrapper to make it root.
These benchmarks were done on a `Intel i5 7200U` processor, on a Debian 12 Docker container.
`sudo` and `doas` were pulled from the Debian repos, `rdo` was compiled locally.
All configs were kept as default, except allow the `wheel` group on both + enable `persist` on doas.
The benchmark can be executed through a Docker container by running:
```
make bench-build bench-run
```

View file

@ -1 +0,0 @@
masters = gentoo

67
rdo.1
View file

@ -1,67 +0,0 @@
.TH RDO 1 "August 2025" "rdo 1.4.3" "User Commands"
.SH NAME
rdo \- execute commands as the superuser
.SH SYNOPSIS
.B rdo
[\fB-\fP]
\fIcommand\fP [\fIargs ...\fP]
.SH DESCRIPTION
The
.B rdo
utility allows a user to run a command as the superuser.
.B rdo
authenticates the user by asking for their password.
Once authenticated,
.B rdo
can optionally cache the successful authentication for a configurable duration.
.PP
The security policy is configured in the
.I /etc/rdo.conf
file. This file determines which users are permitted to use
.BR rdo .
.SH OPTIONS
.TP
.B \-
Read the password from standard input instead of the terminal.
.SH EXIT STATUS
The
.B rdo
utility exits with one of the following values:
.TP
\fB0\fP
The usage message was printed and
.B rdo
exited.
.TP
\fB1\fP
An error occurred.
.PP
Otherwise, the exit status is that of the command executed.
.SH FILES
.TP
.I /etc/rdo.conf
The
.B rdo
configuration file.
.SH EXAMPLES
Run the
.I id
command as the superuser:
.IP
.EX
$ rdo id -u
.EE
.PP
Run a shell as the superuser:
.IP
.EX
$ rdo /bin/sh
.EE
.SH SEE ALSO
.BR doas (1),
.BR sudo (8),
.BR rdo.conf (5)
.SH AUTHOR
The
.B rdo
project was created by sw1tchbl4d3 and was heavily modified by coast and soccera.

48
rdo.c
View file

@ -5,31 +5,41 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#ifdef __linux__
#include <shadow.h>
#endif
#include "readpassphrase.h"
#include "sessions.h"
#define VERSION "1.4.3"
char* getpwhash(struct passwd* pw) {
if (pw->pw_passwd[0] != 'x')
return pw->pw_passwd;
#ifdef __linux__
struct spwd* pw_entry = getspnam(pw->pw_name);
if (!pw_entry || !pw_entry->sp_pwdp)
err(1, "Could not get shadow entry");
return pw_entry->sp_pwdp;
#endif
errx(1, "Could not get hashed password entry");
}
void getconf(FILE* fp, const char* entry, char* result, size_t len_result) {
char* line = NULL;
size_t len = 0;
size_t entry_len = strlen(entry);
fseek(fp, 0, SEEK_SET);
while (getline(&line, &len, fp) != -1) {
if (strncmp(line, entry, entry_len) == 0 && (line[entry_len] == '=')) {
if (strncmp(line, entry, entry_len) == 0 &&
(line[entry_len] == '=')) {
char* value = line + entry_len + 1;
value[strcspn(value, "\n")] = 0;
strncpy(result, value, len_result);
@ -38,45 +48,62 @@ void getconf(FILE* fp, const char* entry, char* result, size_t len_result) {
return;
}
}
free(line);
errx(1, "Could not get '%s' entry in config", entry);
}
void runprog(char** program_argv) {
if (setuid(0) < 0)
err(1, "Could not setuid");
if (setgid(0) < 0)
err(1, "Could not setgid");
putenv("HOME=/root");
// NOTE: this does not return when no error occurred.
execvp(program_argv[0], program_argv);
err(1, "%s", program_argv[0]);
}
int main(int argc, char** argv) {
char groupname[64], wrong_pw_sleep[64], session_ttl[64], password[128];
int sleep_us, tries, ts_ttl;
int read_pw_from_stdin = 0;
if (argc > 1)
read_pw_from_stdin = strcmp(argv[1], "-") == 0;
if (argc == 1 || (read_pw_from_stdin && argc == 2)) {
printf("RootDO version: %s\n\n", VERSION);
printf("Usage: %s [command]\n", argv[0]);
return 0;
}
if (geteuid() != 0)
errx(1, "The rdo binary needs to be installed as SUID.");
int ruid = getuid();
if (ruid == 0)
runprog(&argv[read_pw_from_stdin+1]);
FILE* fp = fopen("/etc/rdo.conf", "r");
if (!fp)
err(1, "Could not open /etc/rdo.conf");
getconf(fp, "group", groupname, sizeof(groupname));
getconf(fp, "wrong_pw_sleep", wrong_pw_sleep, sizeof(wrong_pw_sleep));
getconf(fp, "session_ttl", session_ttl, sizeof(session_ttl));
sleep_us = atoi(wrong_pw_sleep) * 1000;
ts_ttl = atoi(session_ttl) * 60;
fclose(fp);
if (getsession(ts_ttl) == 0 && !read_pw_from_stdin)
runprog(&argv[1]);
struct passwd* pw = getpwuid(ruid);
if (!pw) {
if (errno == 0)
@ -84,40 +111,46 @@ int main(int argc, char** argv) {
else
err(1, "Could not get user info");
}
struct group* current_group_entry = getgrent();
while (current_group_entry) {
if (strcmp(current_group_entry->gr_name, groupname) == 0)
break;
current_group_entry = getgrent();
}
if (!current_group_entry)
errx(1, "The group '%s' does not exist.", groupname);
char* current_member = current_group_entry->gr_mem[0];
for (int i = 1; current_member; i++) {
if (strcmp(current_member, pw->pw_name) == 0)
break;
current_member = current_group_entry->gr_mem[i];
}
if (!current_member)
errx(1, "You are not allowed to execute rdo.");
char* user_hashed_pw = getpwhash(pw);
tries = 0;
while (tries < 3) {
char hostname[64];
gethostname(hostname, sizeof(hostname));
char prompt[256];
snprintf(prompt, sizeof(prompt), "[rdo: (%s@%s) Password]: ", pw->pw_name, hostname);
if (!readpassphrase(prompt, password, sizeof(password), read_pw_from_stdin))
if (!readpassphrase("(rdo) Password: ", password, sizeof(password), read_pw_from_stdin))
err(1, "Could not get passphrase");
char* given_hashed_pw = crypt(password, user_hashed_pw);
memset(password, 0, sizeof(password));
if (!given_hashed_pw)
errx(1, "Could not hash password, does your user have a password?");
if (strcmp(given_hashed_pw, user_hashed_pw) == 0) {
if (!read_pw_from_stdin)
setsession(ts_ttl);
runprog(&argv[read_pw_from_stdin+1]);
}
usleep(sleep_us);
fprintf(stderr, "Wrong password.\n");
tries++;
@ -125,4 +158,3 @@ int main(int argc, char** argv) {
errx(1, "Too many wrong password attempts.");
return 1;
}

View file

@ -1,29 +0,0 @@
.TH RDO.CONF 5 "August 2025" "rdo 1.4.3" "File Formats"
.SH NAME
rdo.conf \- configuration file for rdo
.SH DESCRIPTION
The
.B rdo
utility reads the
.I /etc/rdo.conf
file for its configuration.
.PP
The file consists of
.I variable=value
pairs. Comments are not supported. Leading and trailing whitespace is ignored.
.SH VARIABLES
.TP
.B group=\fIgroup\fP
Specifies the group whose members are allowed to run
.BR rdo .
This is a mandatory variable.
.TP
.B wrong_pw_sleep=\fImilliseconds\fP
The amount of time in milliseconds to wait after a wrong password attempt before prompting again. If not set, the default is 1000. Set to 0 to disable.
.TP
.B session_ttl=\fIminutes\fP
The time to live in minutes for a cached authentication. If a user successfully authenticates, they can run
.B rdo
without a password for this duration. If not set, the default is 5. Set to 0 to disable session caching.
.SH SEE ALSO
.BR rdo (1)