diff --git a/README.md b/README.md index d6642a9..0116acb 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/bsd_impl.c b/bsd_impl.c new file mode 100644 index 0000000..9719888 --- /dev/null +++ b/bsd_impl.c @@ -0,0 +1,272 @@ +#include "li_cil_oc2_common_inet_DefaultSessionLayer.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 [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 */ diff --git a/build-freebsd.sh b/build-freebsd.sh new file mode 100755 index 0000000..a08377f --- /dev/null +++ b/build-freebsd.sh @@ -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" diff --git a/freebsd-notes.md b/freebsd-notes.md new file mode 100644 index 0000000..071e914 --- /dev/null +++ b/freebsd-notes.md @@ -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. diff --git a/oc2rnet-helper.c b/oc2rnet-helper.c new file mode 100644 index 0000000..c6f2d0f --- /dev/null +++ b/oc2rnet-helper.c @@ -0,0 +1,412 @@ +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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); + } +}