413 lines
9.6 KiB
C
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);
|
|
}
|
|
}
|