This commit is contained in:
@@ -9,9 +9,8 @@ The Java ICMP functions don't work on some operating systems without admin/root,
|
||||
- Windows x86_64 and aarch64
|
||||
- GNU/Linux x86_64 and aarch64
|
||||
- macOS x86_64 and aarch64
|
||||
- FreeBSD (helper + JNI bridge, x86_64 and arm64 when built on FreeBSD)
|
||||
|
||||
### Not officially supported, but should still build:
|
||||
## FreeBSD helper workflow
|
||||
|
||||
- Any Linux
|
||||
- Any macOS
|
||||
- Any Windows
|
||||
See [freebsd-notes](./freebsd-notes.md) forsetup details
|
||||
|
||||
272
bsd_impl.c
Normal file
272
bsd_impl.c
Normal file
@@ -0,0 +1,272 @@
|
||||
#include "li_cil_oc2_common_inet_DefaultSessionLayer.h"
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <errno.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/un.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define SOCK_PATH_DEFAULT "/var/run/oc2rnet-helper.sock"
|
||||
#define PROTOCOL_VERSION 1
|
||||
#define MAX_PAYLOAD 2048
|
||||
|
||||
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 int uds_connect(const char *path) {
|
||||
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
if (fd < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
struct sockaddr_un sa;
|
||||
memset(&sa, 0, sizeof(sa));
|
||||
sa.sun_family = AF_UNIX;
|
||||
if (strlcpy(sa.sun_path, path, sizeof(sa.sun_path)) >= sizeof(sa.sun_path)) {
|
||||
close(fd);
|
||||
errno = ENAMETOOLONG;
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (connect(fd, (struct sockaddr *)&sa, sizeof(sa)) < 0) {
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
return fd;
|
||||
}
|
||||
|
||||
JNIEXPORT jbyteArray JNICALL
|
||||
Java_li_cil_oc2_common_inet_DefaultSessionLayer_sendICMP(JNIEnv *env, jobject instance,
|
||||
jbyteArray ip, jbyteArray data,
|
||||
jint size, jint timeout) {
|
||||
(void)instance;
|
||||
|
||||
if (size < 0 || size > MAX_PAYLOAD) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const char *sock_path = getenv("OC2RNET_SOCK");
|
||||
if (sock_path == NULL) {
|
||||
sock_path = SOCK_PATH_DEFAULT;
|
||||
}
|
||||
|
||||
jbyte *addr = (*env)->GetByteArrayElements(env, ip, NULL);
|
||||
if (addr == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
jbyte *payload = (*env)->GetByteArrayElements(env, data, NULL);
|
||||
if (payload == NULL) {
|
||||
(*env)->ReleaseByteArrayElements(env, ip, addr, JNI_ABORT);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int fd = uds_connect(sock_path);
|
||||
if (fd < 0) {
|
||||
(*env)->ReleaseByteArrayElements(env, ip, addr, JNI_ABORT);
|
||||
(*env)->ReleaseByteArrayElements(env, data, payload, JNI_ABORT);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
struct PingReq req;
|
||||
memset(&req, 0, sizeof(req));
|
||||
req.version = PROTOCOL_VERSION;
|
||||
req.size = (uint16_t)size;
|
||||
memcpy(&req.ip_be, addr, sizeof(req.ip_be));
|
||||
req.timeout_ms = (uint32_t)timeout;
|
||||
|
||||
ssize_t written = write(fd, &req, sizeof(req));
|
||||
if (written != (ssize_t)sizeof(req)) {
|
||||
goto fail;
|
||||
}
|
||||
|
||||
if (size > 0) {
|
||||
written = write(fd, payload, (size_t)size);
|
||||
if (written != size) {
|
||||
goto fail;
|
||||
}
|
||||
}
|
||||
|
||||
struct PingRes res;
|
||||
ssize_t received = read(fd, &res, sizeof(res));
|
||||
if (received != (ssize_t)sizeof(res)) {
|
||||
goto fail;
|
||||
}
|
||||
|
||||
if (res.status != 0 || res.size > (uint16_t)size) {
|
||||
goto fail;
|
||||
}
|
||||
|
||||
jbyteArray out = (*env)->NewByteArray(env, res.size);
|
||||
if (out == NULL) {
|
||||
goto fail;
|
||||
}
|
||||
|
||||
jbyte *outbuf = malloc(res.size);
|
||||
if (outbuf == NULL) {
|
||||
(*env)->DeleteLocalRef(env, out);
|
||||
goto fail;
|
||||
}
|
||||
|
||||
received = read(fd, outbuf, res.size);
|
||||
if (received != res.size) {
|
||||
free(outbuf);
|
||||
(*env)->DeleteLocalRef(env, out);
|
||||
goto fail;
|
||||
}
|
||||
|
||||
(*env)->SetByteArrayRegion(env, out, 0, res.size, outbuf);
|
||||
free(outbuf);
|
||||
|
||||
close(fd);
|
||||
(*env)->ReleaseByteArrayElements(env, ip, addr, JNI_ABORT);
|
||||
(*env)->ReleaseByteArrayElements(env, data, payload, JNI_ABORT);
|
||||
return out;
|
||||
|
||||
fail:
|
||||
close(fd);
|
||||
(*env)->ReleaseByteArrayElements(env, ip, addr, JNI_ABORT);
|
||||
(*env)->ReleaseByteArrayElements(env, data, payload, JNI_ABORT);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
#ifdef CLITEST
|
||||
|
||||
static ssize_t write_all(int fd, const void *buffer, size_t length) {
|
||||
const uint8_t *ptr = buffer;
|
||||
size_t total = 0;
|
||||
while (total < length) {
|
||||
ssize_t written = write(fd, ptr + total, length - total);
|
||||
if (written < 0) {
|
||||
if (errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
total += (size_t)written;
|
||||
}
|
||||
return (ssize_t)total;
|
||||
}
|
||||
|
||||
static ssize_t read_all(int fd, void *buffer, size_t length) {
|
||||
uint8_t *ptr = buffer;
|
||||
size_t total = 0;
|
||||
while (total < length) {
|
||||
ssize_t read_bytes = read(fd, ptr + total, length - total);
|
||||
if (read_bytes == 0) {
|
||||
return (ssize_t)total;
|
||||
}
|
||||
if (read_bytes < 0) {
|
||||
if (errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
total += (size_t)read_bytes;
|
||||
}
|
||||
return (ssize_t)total;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc < 2) {
|
||||
fprintf(stderr, "usage: %s <ipv4> [payload]\n", argv[0]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const char *target_ip = argv[1];
|
||||
const char *payload_str = (argc >= 3) ? argv[2] : "oc2r";
|
||||
size_t payload_len = strlen(payload_str);
|
||||
|
||||
if (payload_len > MAX_PAYLOAD) {
|
||||
fprintf(stderr, "payload too large (max %d)\n", MAX_PAYLOAD);
|
||||
return 1;
|
||||
}
|
||||
|
||||
struct in_addr in_addr_target;
|
||||
if (inet_pton(AF_INET, target_ip, &in_addr_target) != 1) {
|
||||
perror("inet_pton");
|
||||
return 1;
|
||||
}
|
||||
|
||||
const char *sock_path = getenv("OC2RNET_SOCK");
|
||||
if (sock_path == NULL || *sock_path == '\0') {
|
||||
sock_path = SOCK_PATH_DEFAULT;
|
||||
}
|
||||
|
||||
int fd = uds_connect(sock_path);
|
||||
if (fd < 0) {
|
||||
perror("uds_connect");
|
||||
return 1;
|
||||
}
|
||||
|
||||
struct PingReq req;
|
||||
memset(&req, 0, sizeof(req));
|
||||
req.version = PROTOCOL_VERSION;
|
||||
req.size = (uint16_t)payload_len;
|
||||
req.ip_be = in_addr_target.s_addr;
|
||||
req.timeout_ms = 1000;
|
||||
|
||||
if (write_all(fd, &req, sizeof(req)) != (ssize_t)sizeof(req)) {
|
||||
perror("write request");
|
||||
close(fd);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (payload_len > 0) {
|
||||
if (write_all(fd, payload_str, payload_len) != (ssize_t)payload_len) {
|
||||
perror("write payload");
|
||||
close(fd);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
struct PingRes res;
|
||||
if (read_all(fd, &res, sizeof(res)) != (ssize_t)sizeof(res)) {
|
||||
perror("read response");
|
||||
close(fd);
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("status=%d size=%u\n", res.status, res.size);
|
||||
|
||||
if (res.status == 0 && res.size > 0) {
|
||||
uint8_t *buffer = malloc(res.size);
|
||||
if (buffer == NULL) {
|
||||
fprintf(stderr, "malloc failed\n");
|
||||
close(fd);
|
||||
return 1;
|
||||
}
|
||||
if (read_all(fd, buffer, res.size) != res.size) {
|
||||
perror("read payload");
|
||||
free(buffer);
|
||||
close(fd);
|
||||
return 1;
|
||||
}
|
||||
|
||||
fwrite(buffer, 1, res.size, stdout);
|
||||
putchar('\n');
|
||||
free(buffer);
|
||||
}
|
||||
|
||||
close(fd);
|
||||
return 0;
|
||||
}
|
||||
|
||||
#endif /* CLITEST */
|
||||
28
build-freebsd.sh
Executable file
28
build-freebsd.sh
Executable file
@@ -0,0 +1,28 @@
|
||||
#!/bin/sh -e
|
||||
# shellcheck disable=2086
|
||||
|
||||
[ -z "$CFLAGS" ] && CFLAGS='-O2 -pipe -Wall -Wextra -pedantic'
|
||||
[ -z "$CLANG" ] && CLANG=cc
|
||||
[ -z "$STRIP" ] && STRIP=strip
|
||||
|
||||
if [ -z "$JAVA_HOME" ]; then
|
||||
printf "JAVA_HOME must be set to a JDK with JNI headers (e.g. /usr/local/openjdk17)\n" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ARCH=$(uname -m)
|
||||
case "$ARCH" in
|
||||
amd64|x86_64) OUT_ARCH="x86_64" ;;
|
||||
aarch64|arm64) OUT_ARCH="arm64" ;;
|
||||
*)
|
||||
printf "Unsupported FreeBSD architecture: %s\n" "$ARCH" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
mkdir -p build
|
||||
|
||||
$CLANG $CFLAGS -std=c99 -fPIC -Ijni-headers -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/freebsd" \
|
||||
bsd_impl.c -shared -o "build/liboc2rnet-bsd-${OUT_ARCH}.so"
|
||||
|
||||
$STRIP "build/liboc2rnet-bsd-${OUT_ARCH}.so"
|
||||
91
freebsd-notes.md
Normal file
91
freebsd-notes.md
Normal file
@@ -0,0 +1,91 @@
|
||||
# FreeBSD Support Improvements
|
||||
|
||||
- **Native Build Artefacts**
|
||||
Update `oc2r-native-networking/build.sh` so producing
|
||||
`liboc2rnet-bsd-{x86_64,arm64}.so` is part of the normal build pipeline. At
|
||||
the moment the script never emits BSD binaries, so new resources would be
|
||||
missing from CI outputs.
|
||||
|
||||
## Build and install (FreeBSD)
|
||||
|
||||
1. **Install prerequisites**
|
||||
```sh
|
||||
pkg install openjdk17 clang gmake
|
||||
export JAVA_HOME=/usr/local/openjdk17 # adjust if your JDK lives elsewhere
|
||||
```
|
||||
|
||||
2. **Build the FreeBSD JNI bridge**
|
||||
```sh
|
||||
cd oc2r-native-networking
|
||||
./build-freebsd.sh
|
||||
cp build/liboc2rnet-bsd-*.so ../oc2r/src/main/resources/natives/bsd/ # Adjust to your oc2r sources location
|
||||
```
|
||||
|
||||
3. **Compile and install the helper**
|
||||
```sh
|
||||
cc -O2 -Wall -Wextra -pedantic oc2rnet-helper.c -o oc2rnet-helper
|
||||
install -o root -g wheel -m 0755 oc2rnet-helper /usr/local/libexec/oc2rnet-helper
|
||||
chmod u+s /usr/local/libexec/oc2rnet-helper
|
||||
```
|
||||
|
||||
## Launch helper and validation
|
||||
|
||||
4. **Launch the helper**
|
||||
- Foreground check:
|
||||
```sh
|
||||
/usr/local/libexec/oc2rnet-helper
|
||||
```
|
||||
- Background:
|
||||
```sh
|
||||
daemon -r -f /usr/local/libexec/oc2rnet-helper
|
||||
```
|
||||
|
||||
5. **Validate the socket and raw privileges**
|
||||
```sh
|
||||
ls -l /var/run/oc2rnet-helper.sock
|
||||
sockstat -l | grep oc2rnet
|
||||
```
|
||||
|
||||
6. **Test the jni against helper**
|
||||
```sh
|
||||
cc -DCLITEST -O2 -Wall -Wextra -pedantic -Ijni-headers -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/freebsd" bsd_impl.c -o pingtest
|
||||
./pingtest 9.9.9.9
|
||||
```
|
||||
Expect a successful payload echo when the helper is reachable; a `NULL`
|
||||
result signals the helper was unavailable, matching the mod’s fallback
|
||||
behaviour.
|
||||
|
||||
7. **(optional) Kill daemon**
|
||||
```sh
|
||||
kill "$(pgrep -fx 'daemon -r -f /usr/local/libexec/oc2rnet-helper')"
|
||||
|
||||
```
|
||||
|
||||
## Recompile java mod with freebsd compat
|
||||
|
||||
1. If not already done copy the so file(s) compiled in step 2 of the build section
|
||||
|
||||
2. (optional) Change o2cr mod version in `gradle.properties`
|
||||
|
||||
3. Run `./gradlew build` in the oc2r sources files modified to support FreeBSD
|
||||
|
||||
4. Get the file located in `./build/libs/oc2r-*-all.jar` and put it in your `mods` directory
|
||||
|
||||
## Important note
|
||||
|
||||
Run the helper with `daemon(8)` or an `rc.d` script so it stays available. The
|
||||
JNI bridge will fall back to returning `null` if the helper is unreachable.
|
||||
|
||||
## Optional helper configuration
|
||||
|
||||
Set these environment variables before launching the helper to adjust runtime
|
||||
behaviour:
|
||||
|
||||
- `OC2RNET_SOCKET_MODE` (octal, default `0666`) — override the UNIX socket mode.
|
||||
- `OC2RNET_SOCKET_GROUP` — chown the socket to this group after `bind(2)`.
|
||||
- `OC2RNET_DROP_USER` (default `nobody`) — unprivileged account the helper
|
||||
switches to once initialisation is complete.
|
||||
- `OC2RNET_SOCK` (default `/var/run/oc2rnet-helper.sock`) — alternate socket
|
||||
path for jails or non-standard hierarchies.
|
||||
- `OC2RNET_NO_CAPSICUM` — set to any value to skip `cap_enter()` when Capsicum
|
||||
blocks raw sockets on older releases.
|
||||
412
oc2rnet-helper.c
Normal file
412
oc2rnet-helper.c
Normal file
@@ -0,0 +1,412 @@
|
||||
#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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user