Add support for FreeBSD
Some checks failed
build / build (push) Has been cancelled

This commit is contained in:
2025-10-31 16:29:40 +01:00
parent c205783caa
commit 27ad967f41
5 changed files with 806 additions and 4 deletions

View File

@@ -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
View 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
View 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
View 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 mods 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
View 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);
}
}