Files
oc2r-native-networking/oc2rnet-helper.c
Jika 27ad967f41
Some checks failed
build / build (push) Has been cancelled
Add support for FreeBSD
2025-10-31 16:29:40 +01:00

413 lines
9.6 KiB
C

#define _GNU_SOURCE
#include <arpa/inet.h>
#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <grp.h>
#include <sys/types.h>
#include <stdint.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <netinet/ip_icmp.h>
#include <poll.h>
#include <pwd.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/capsicum.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/un.h>
#include <time.h>
#include <unistd.h>
#define DEFAULT_SOCKET_PATH "/var/run/oc2rnet-helper.sock"
#define BACKLOG 16
#define MAX_PAYLOAD 2048
#define ICMP_HEADER 8
struct PingReq {
uint8_t version;
uint8_t reserved;
uint16_t size;
uint32_t ip_be;
uint32_t timeout_ms;
} __attribute__((packed));
struct PingRes {
int32_t status;
uint16_t size;
uint16_t reserved;
} __attribute__((packed));
static uint16_t checksum(void *buffer, size_t length) {
uint32_t sum = 0;
uint16_t *data = buffer;
while (length > 1) {
sum += *data++;
length -= 2;
}
if (length == 1) {
sum += *(uint8_t *)data;
}
sum = (sum >> 16) + (sum & 0xffff);
sum += (sum >> 16);
return (uint16_t)~sum;
}
static ssize_t read_full(int fd, void *buffer, size_t length) {
uint8_t *ptr = buffer;
size_t total = 0;
while (total < length) {
ssize_t n = read(fd, ptr + total, length - total);
if (n == 0) {
return total;
}
if (n < 0) {
if (errno == EINTR) {
continue;
}
return -1;
}
total += (size_t)n;
}
return (ssize_t)total;
}
static ssize_t write_full(int fd, const void *buffer, size_t length) {
const uint8_t *ptr = buffer;
size_t total = 0;
while (total < length) {
ssize_t n = write(fd, ptr + total, length - total);
if (n < 0) {
if (errno == EINTR) {
continue;
}
return -1;
}
total += (size_t)n;
}
return (ssize_t)total;
}
static int do_icmp_echo(int raw_fd, uint32_t ip_be, const uint8_t *data,
size_t size, uint32_t timeout_ms, uint8_t *out,
size_t *out_size) {
if (size > 1472) {
size = 1472;
}
(void)ip_be; // destination is set via connect(); unused here
uint8_t packet[ICMP_HEADER + 1472];
struct icmp *icmp = (struct icmp *)packet;
memset(packet, 0, sizeof(packet));
icmp->icmp_type = ICMP_ECHO;
icmp->icmp_code = 0;
icmp->icmp_id = (uint16_t)getpid();
icmp->icmp_seq = 1;
memcpy(icmp->icmp_data, data, size);
icmp->icmp_cksum = 0;
icmp->icmp_cksum = checksum(packet, ICMP_HEADER + size);
// In capability mode you cannot specify a destination in sendto().
// Use send() on the already-connected raw socket.
if (send(raw_fd, packet, ICMP_HEADER + size, 0) < 0) {
warn("send");
return -2;
}
struct pollfd pfd = {
.fd = raw_fd,
.events = POLLIN,
};
int poll_res = poll(&pfd, 1, (int)timeout_ms);
if (poll_res == 0) {
return -1;
}
if (poll_res < 0) {
return -3;
}
uint8_t recvbuf[2048];
ssize_t received = recv(raw_fd, recvbuf, sizeof(recvbuf), 0);
if (received < 0) {
return -3;
}
if ((size_t)received < sizeof(struct ip) + ICMP_HEADER) {
return -3;
}
struct ip *ip_header = (struct ip *)recvbuf;
size_t ip_header_len = (size_t)(ip_header->ip_hl << 2);
if (ip_header_len + ICMP_HEADER > (size_t)received) {
return -3;
}
struct icmp *reply = (struct icmp *)(recvbuf + ip_header_len);
if (reply->icmp_type != ICMP_ECHOREPLY) {
return -3;
}
size_t payload_size = (size_t)received - ip_header_len - ICMP_HEADER;
if (payload_size > *out_size) {
payload_size = *out_size;
}
memcpy(out, reply->icmp_data, payload_size);
*out_size = payload_size;
return 0;
}
static void drop_privileges(void);
static int handle_client(int client, uint32_t ip_be, const uint8_t *buf,
size_t buf_sz, uint32_t timeout_ms, int capsicum) {
struct sockaddr_in dst;
memset(&dst, 0, sizeof(dst));
dst.sin_family = AF_INET;
dst.sin_addr.s_addr = ip_be;
int raw_fd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
if (raw_fd < 0) {
warn("raw socket");
return -4;
}
if (connect(raw_fd, (struct sockaddr *)&dst, sizeof(dst)) != 0) {
warn("connect(raw_fd)");
close(raw_fd);
return -4;
}
if (capsicum) {
cap_rights_t client_rights;
cap_rights_init(&client_rights, CAP_READ, CAP_WRITE);
(void)cap_rights_limit(client, &client_rights);
cap_rights_t raw_rights;
cap_rights_init(&raw_rights, CAP_EVENT, CAP_RECV, CAP_SEND, CAP_CONNECT,
CAP_READ, CAP_WRITE);
(void)cap_rights_limit(raw_fd, &raw_rights);
}
drop_privileges();
if (capsicum) {
if (cap_enter() != 0) {
warn("cap_enter");
}
}
uint8_t out[1472];
size_t out_sz = sizeof(out);
int st = do_icmp_echo(raw_fd, ip_be, buf, buf_sz, timeout_ms, out, &out_sz);
close(raw_fd);
struct PingRes res;
res.status = st;
res.size = (uint16_t)((st == 0) ? out_sz : 0);
res.reserved = 0;
if (write_full(client, &res, sizeof(res)) != (ssize_t)sizeof(res)) {
return -4;
}
if (st == 0 && out_sz > 0) {
(void)write_full(client, out, out_sz);
}
return 0;
}
static const char *socket_path(void) {
const char *override = getenv("OC2RNET_SOCK");
if (override && *override) {
return override;
}
return DEFAULT_SOCKET_PATH;
}
static mode_t socket_mode(void) {
const char *override = getenv("OC2RNET_SOCKET_MODE");
if (!override || !*override) {
return (mode_t)0666;
}
char *end = NULL;
errno = 0;
long parsed = strtol(override, &end, 8);
if (errno != 0 || end == override || parsed < 0 || parsed > 0777) {
warnx("invalid OC2RNET_SOCKET_MODE '%s', falling back to 0666", override);
return (mode_t)0666;
}
return (mode_t)parsed;
}
static void adjust_socket_ownership(const char *path) {
const char *group_name = getenv("OC2RNET_SOCKET_GROUP");
if (!group_name || !*group_name) {
if (chmod(path, socket_mode()) != 0) {
warn("chmod socket");
}
return;
}
struct group *grp = getgrnam(group_name);
if (grp == NULL) {
warn("getgrnam(%s)", group_name);
if (chmod(path, socket_mode()) != 0) {
warn("chmod socket");
}
return;
}
if (chown(path, (uid_t)-1, grp->gr_gid) != 0) {
warn("chown socket");
}
if (chmod(path, socket_mode()) != 0) {
warn("chmod socket");
}
}
static void drop_privileges(void) {
const char *user_override = getenv("OC2RNET_DROP_USER");
const char *target_user =
(user_override && *user_override) ? user_override : "nobody";
struct passwd *pw = getpwnam(target_user);
if (!pw) {
err(1, "getpwnam(%s)", target_user);
}
if (setgroups(1, &pw->pw_gid) != 0) {
err(1, "setgroups");
}
if (setgid(pw->pw_gid) != 0) {
err(1, "setgid");
}
if (setuid(pw->pw_uid) != 0) {
err(1, "setuid");
}
}
static int use_capsicum(void) {
const char *disable = getenv("OC2RNET_NO_CAPSICUM");
if (disable && *disable) {
return 0;
}
return 1;
}
int main(void) {
signal(SIGPIPE, SIG_IGN);
signal(SIGCHLD, SIG_IGN);
const char *sock_path = socket_path();
unlink(sock_path);
int listen_fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (listen_fd < 0) {
err(1, "socket");
}
struct sockaddr_un addr;
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
if (strlcpy(addr.sun_path, sock_path, sizeof(addr.sun_path)) >=
sizeof(addr.sun_path)) {
errx(1, "socket path too long: %s", sock_path);
}
if (bind(listen_fd, (struct sockaddr *)&addr, sizeof(addr)) != 0) {
err(1, "bind");
}
adjust_socket_ownership(sock_path);
if (listen(listen_fd, BACKLOG) != 0) {
err(1, "listen");
}
int capsicum = use_capsicum();
if (capsicum) {
// Accepted sockets inherit a subset of the listening socket's rights.
// Give the listener only what is needed so clients can read/write.
cap_rights_t listen_rights;
cap_rights_init(&listen_rights, CAP_ACCEPT, CAP_EVENT, CAP_READ, CAP_WRITE);
if (cap_rights_limit(listen_fd, &listen_rights) != 0) {
err(1, "cap_rights_limit(listen_fd)");
}
}
for (;;) {
int client = accept(listen_fd, NULL, NULL);
if (client < 0) {
if (errno == EINTR) {
continue;
}
warn("accept");
continue;
}
if (capsicum) {
// Immediately restrict the accepted socket to read/write only.
cap_rights_t client_rights;
cap_rights_init(&client_rights, CAP_READ, CAP_WRITE);
if (cap_rights_limit(client, &client_rights) != 0) {
warn("cap_rights_limit(client)");
close(client);
continue;
}
}
struct PingReq req;
ssize_t r = read_full(client, &req, sizeof(req));
if (r != (ssize_t)sizeof(req)) {
struct PingRes res_fail = {.status = -4, .size = 0, .reserved = 0};
write_full(client, &res_fail, sizeof(res_fail));
close(client);
continue;
}
if (req.version != 1 || req.size > MAX_PAYLOAD) {
struct PingRes res_fail = {.status = -4, .size = 0, .reserved = 0};
write_full(client, &res_fail, sizeof(res_fail));
close(client);
continue;
}
uint8_t buf[MAX_PAYLOAD];
if (req.size > 0) {
ssize_t payload_read = read_full(client, buf, req.size);
if (payload_read != req.size) {
struct PingRes res_fail = {.status = -4, .size = 0, .reserved = 0};
write_full(client, &res_fail, sizeof(res_fail));
close(client);
continue;
}
}
pid_t pid = fork();
if (pid < 0) {
warn("fork");
close(client);
continue;
}
if (pid == 0) {
(void)handle_client(client, req.ip_be, buf, req.size, req.timeout_ms,
capsicum);
close(client);
_exit(0);
}
close(client);
}
}