diff --git a/README.md b/README.md index 8d7fccd..6322c89 100644 --- a/README.md +++ b/README.md @@ -93,3 +93,19 @@ The OPNsense page **Services -> RustProxy** exposes the runtime listeners used b - Allow remote admin: on by default so the RustProxy panel can be reached from other hosts; OPNsense firewall rules still decide which networks can connect The service runs as the unprivileged `rustproxy` user and keeps `/var/db/rustproxy` mode `0700`, as required by RustProxy. Package installation restarts `configd` so `configctl rustproxy ...` actions are immediately available even after direct `pkg add`. + +## FreeBSD/OPNsense-only RustProxy patches + +Version-specific application patches live under `builder/freebsd/patches//`. +They are **not** applied to `/opt/opn-rustproxy/source/RustProxy` and are never pushed back to the RustProxy repository. +`builder/debian/build-docker-release.sh` creates an isolated source copy under `dist/.build-source/`, runs `builder/freebsd/apply-patches.sh` there, and mounts only that patched copy into the Docker build container. + +For RustProxy `0.20.0` the patch set is: + +`builder/freebsd/patches/0.20.0/0001-opnsense-build.patch` + +Set `APPLY_FREEBSD_PATCHES=0` in `/etc/rustproxy-opnsense-builder.env` only when you explicitly want to test the unpatched upstream source. + +### Runtime PID directory + +The OPNsense rc.d service creates `/var/run/rustproxy` as `rustproxy:rustproxy` (0750) on every start. Both supervisor and child PID files live there so `daemon -u rustproxy` can run without root-owned PID-file errors. diff --git a/builder.env.example b/builder.env.example index b2946fd..6523e91 100644 --- a/builder.env.example +++ b/builder.env.example @@ -21,6 +21,10 @@ FREEBSD_ARCH=freebsd:15:x86:64 OPNSENSE_ABI=26.7 DOCKER_IMAGE=rustproxy-freebsd15-builder:15.1-rust1.88 +# Apply version-specific RustProxy patches only inside the isolated OPNsense/FreeBSD build copy. +# The checkout in APP_WORKDIR is never modified. +APPLY_FREEBSD_PATCHES=1 + # Run native Linux tests before the FreeBSD cross build (1=yes, 0=no). RUN_LINUX_TESTS=1 diff --git a/builder/debian/build-docker-release.sh b/builder/debian/build-docker-release.sh index 31e7a4e..324aeba 100755 --- a/builder/debian/build-docker-release.sh +++ b/builder/debian/build-docker-release.sh @@ -7,14 +7,40 @@ FREEBSD_RELEASE="${FREEBSD_RELEASE:-15.1-RELEASE}" DOCKER_IMAGE="${DOCKER_IMAGE:-rustproxy-freebsd15-builder:15.1-rust1.88}" DOCKER_CACHE_DIR="${DOCKER_CACHE_DIR:-/opt/opn-rustproxy/cache}" RUN_LINUX_TESTS="${RUN_LINUX_TESTS:-1}" +APPLY_FREEBSD_PATCHES="${APPLY_FREEBSD_PATCHES:-1}" command -v docker >/dev/null 2>&1 || { echo "Docker is required" >&2; exit 1; } +command -v rsync >/dev/null 2>&1 || { echo "rsync is required" >&2; exit 1; } [[ -f "$SOURCE_DIR/Cargo.toml" ]] || { echo "Invalid RustProxy source: $SOURCE_DIR" >&2; exit 1; } -mkdir -p "$DOCKER_CACHE_DIR/cargo" "$DOCKER_CACHE_DIR/target" "$ROOT/dist/docker-build" +mkdir -p "$DOCKER_CACHE_DIR/cargo" "$DOCKER_CACHE_DIR/target" "$ROOT/dist/docker-build" "$ROOT/dist/.build-source" uid="$(id -u)" gid="$(id -g)" +# Never modify the upstream checkout. FreeBSD/OPNsense-only patches are +# applied to an isolated source copy which exists only for this build. +BUILD_SOURCE_DIR="$SOURCE_DIR" +PATCH_WORK_DIR="" +cleanup_build_source() +{ + if [[ -n "$PATCH_WORK_DIR" ]] && [[ -d "$PATCH_WORK_DIR" ]]; then + rm -rf "$PATCH_WORK_DIR" + fi +} +trap cleanup_build_source EXIT + +if [[ "$APPLY_FREEBSD_PATCHES" == "1" ]]; then + PATCH_WORK_DIR="$(mktemp -d "$ROOT/dist/.build-source/rustproxy.XXXXXX")" + rsync -a --delete \ + --exclude='.git/' \ + --exclude='target/' \ + "$SOURCE_DIR/" "$PATCH_WORK_DIR/" + "$ROOT/builder/freebsd/apply-patches.sh" "$PATCH_WORK_DIR" + BUILD_SOURCE_DIR="$PATCH_WORK_DIR" +else + echo "FreeBSD/OPNsense source patches disabled (APPLY_FREEBSD_PATCHES=$APPLY_FREEBSD_PATCHES)." +fi + # Rebuild is cheap after the first run because the FreeBSD sysroot layer is cached. docker build \ --build-arg "FREEBSD_RELEASE=$FREEBSD_RELEASE" \ @@ -27,7 +53,7 @@ run_args=( --user "$uid:$gid" -e CARGO_HOME=/cargo -e CARGO_TARGET_DIR=/target - -v "$SOURCE_DIR:/src:ro" + -v "$BUILD_SOURCE_DIR:/src:ro" -v "$DOCKER_CACHE_DIR/cargo:/cargo" -v "$DOCKER_CACHE_DIR/target:/target" -v "$ROOT/dist/docker-build:/out" diff --git a/builder/freebsd/apply-patches.sh b/builder/freebsd/apply-patches.sh new file mode 100755 index 0000000..a364839 --- /dev/null +++ b/builder/freebsd/apply-patches.sh @@ -0,0 +1,51 @@ +#!/bin/bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SOURCE_DIR="${1:?usage: apply-patches.sh /path/to/RustProxy-source}" + +[[ -f "$SOURCE_DIR/Cargo.toml" ]] || { + echo "Missing Cargo.toml in RustProxy source: $SOURCE_DIR" >&2 + exit 1 +} + +command -v git >/dev/null 2>&1 || { echo "git is required to apply FreeBSD patches" >&2; exit 1; } +command -v python3 >/dev/null 2>&1 || { echo "python3 is required to detect RustProxy version" >&2; exit 1; } + +version="$(python3 - "$SOURCE_DIR/Cargo.toml" <<'PY' +import sys +import tomllib +from pathlib import Path +with Path(sys.argv[1]).open('rb') as f: + data = tomllib.load(f) +print(data['package']['version']) +PY +)" + +PATCH_DIR="$ROOT/patches/$version" +if [[ ! -d "$PATCH_DIR" ]]; then + echo "No FreeBSD/OPNsense patch set for RustProxy $version; building upstream source unchanged." + exit 0 +fi + +shopt -s nullglob +patches=("$PATCH_DIR"/*.patch) +if (( ${#patches[@]} == 0 )); then + echo "FreeBSD patch directory is empty for RustProxy $version: $PATCH_DIR" >&2 + exit 1 +fi + +echo "Applying FreeBSD/OPNsense patch set for RustProxy $version to isolated build source..." +for patch_file in "${patches[@]}"; do + patch_name="$(basename "$patch_file")" + if git -C "$SOURCE_DIR" apply --check "$patch_file" 2>/dev/null; then + git -C "$SOURCE_DIR" apply "$patch_file" + echo " applied: $patch_name" + elif git -C "$SOURCE_DIR" apply --reverse --check "$patch_file" 2>/dev/null; then + echo " already present: $patch_name" + else + echo "Patch does not apply cleanly: $patch_name" >&2 + echo "RustProxy source version: $version" >&2 + exit 1 + fi +done diff --git a/builder/freebsd/patches/0.20.0/0001-opnsense-build.patch b/builder/freebsd/patches/0.20.0/0001-opnsense-build.patch new file mode 100644 index 0000000..f13a6b1 --- /dev/null +++ b/builder/freebsd/patches/0.20.0/0001-opnsense-build.patch @@ -0,0 +1,75 @@ +diff --git a/rust-toolchain.toml b/rust-toolchain.toml +index 05e6ca1..fdda5b9 100644 +--- a/rust-toolchain.toml ++++ b/rust-toolchain.toml +@@ -1,4 +1,4 @@ + [toolchain] +-channel = "stable" ++channel = "1.88.0" + profile = "minimal" + components = ["rustfmt", "clippy"] +diff --git a/src/cache.rs b/src/cache.rs +index fcd6f36..82332e5 100644 +--- a/src/cache.rs ++++ b/src/cache.rs +@@ -3774,9 +3774,10 @@ mod tests { + .unwrap(), + ); + h.insert("age", HeaderValue::from_static("3")); +- let plan = plan(StatusCode::OK, &h, &settings, Duration::from_millis(250)).unwrap(); +- assert!(plan.initial_age >= Duration::from_secs(10)); +- assert!(plan.initial_age < Duration::from_secs(12)); ++ let cache_plan = ++ plan(StatusCode::OK, &h, &settings, Duration::from_millis(250)).unwrap(); ++ assert!(cache_plan.initial_age >= Duration::from_secs(10)); ++ assert!(cache_plan.initial_age < Duration::from_secs(12)); + let fresh = plan( + StatusCode::OK, + &headers("max-age=60"), +diff --git a/src/proxy/websocket.rs b/src/proxy/websocket.rs +index a25776e..c1d3519 100644 +--- a/src/proxy/websocket.rs ++++ b/src/proxy/websocket.rs +@@ -653,7 +653,11 @@ mod tests { + .unwrap(); + request + .extensions_mut() +- .insert("websocket".parse::().unwrap()); ++ .insert( ++ "websocket" ++ .parse::() ++ .unwrap_or_else(|_| panic!("websocket is a valid HTTP/3 protocol token")), ++ ); + request + } + #[test] +@@ -702,7 +706,11 @@ mod tests { + let mut request = h3_request(); + request + .extensions_mut() +- .insert("connect-udp".parse::().unwrap()); ++ .insert( ++ "connect-udp" ++ .parse::() ++ .unwrap_or_else(|_| panic!("connect-udp is a valid HTTP/3 protocol token")), ++ ); + assert!(validate_h3(&request).is_err()); + } + #[test] +diff --git a/tests/http3.rs b/tests/http3.rs +index c5990bc..6f775ea 100644 +--- a/tests/http3.rs ++++ b/tests/http3.rs +@@ -14,7 +14,11 @@ fn rfc9220_extended_connect_is_recognized() { + .unwrap(); + request + .extensions_mut() +- .insert("websocket".parse::().unwrap()); ++ .insert( ++ "websocket" ++ .parse::() ++ .unwrap_or_else(|_| panic!("websocket is a valid HTTP/3 protocol token")), ++ ); + + assert_eq!(h3_protocol(&request), Some("websocket")); + let handshake = validate_h3(&request).expect("valid RFC 9220 bootstrap"); diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index c466255..f60e706 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -35,3 +35,9 @@ and compresses it with zstd to `.pkg`. ``` The exact RustProxy SHA and package version are written to `dist/BUILD-INFO.txt`. + +## Build-only FreeBSD patch layer + +RustProxy upstream is kept unchanged. Before Docker compilation, the Debian builder copies the checked-out source into a temporary directory under `dist/.build-source/` and applies version-specific patches from `builder/freebsd/patches//`. Docker receives the temporary copy read-only. The temporary tree is removed after the build. + +For RustProxy 0.20.0 the patch is `builder/freebsd/patches/0.20.0/0001-opnsense-build.patch`. This makes the FreeBSD/OPNsense compatibility changes part of the packaging repository rather than the RustProxy repository. diff --git a/docs/FIRST_RUN.md b/docs/FIRST_RUN.md index dead62e..cb7ddec 100644 --- a/docs/FIRST_RUN.md +++ b/docs/FIRST_RUN.md @@ -154,3 +154,35 @@ The RustProxy administration UI is reachable from other hosts by default on port Direct `pkg add` is supported: the package post-install script restarts OPNsense `configd`, flushes the plugin cache and renders `/usr/local/etc/rustproxy/opnsense.conf` so `configctl rustproxy status/start/stop` is available immediately. RustProxy requires its data directory to be private. The plugin enforces owner `rustproxy:rustproxy` and mode `0700` on `/var/db/rustproxy` before every start. + +## FreeBSD patch set + +The OPNsense builder automatically applies version-specific RustProxy patches to a temporary build copy only. The application checkout remains clean: + +```bash +cd /opt/opn-rustproxy/source/RustProxy +git status +``` + +For RustProxy 0.20.0 the builder uses: + +```text +builder/freebsd/patches/0.20.0/0001-opnsense-build.patch +``` + +The normal setting is: + +```bash +APPLY_FREEBSD_PATCHES=1 +``` + +During a build you should see: + +```text +Applying FreeBSD/OPNsense patch set for RustProxy 0.20.0 to isolated build source... + applied: 0001-opnsense-build.patch +``` + +## Runtime directory + +`/var/run/rustproxy` is created automatically as `rustproxy:rustproxy` with mode 0750 during package installation and before each service start. PID files are `daemon.pid` and `rustproxy.pid` inside that directory. diff --git a/opnsense-plugin/www/rustproxy/+POST_INSTALL.pre b/opnsense-plugin/www/rustproxy/+POST_INSTALL.pre index d33354b..1b6509d 100755 --- a/opnsense-plugin/www/rustproxy/+POST_INSTALL.pre +++ b/opnsense-plugin/www/rustproxy/+POST_INSTALL.pre @@ -12,6 +12,7 @@ fi /usr/sbin/chown rustproxy:rustproxy /var/db/rustproxy /bin/chmod 0700 /var/db/rustproxy /usr/bin/install -d -o rustproxy -g rustproxy -m 0750 /var/log/rustproxy +/usr/bin/install -d -o rustproxy -g rustproxy -m 0750 /var/run/rustproxy /usr/bin/install -d -o root -g wheel -m 0755 /usr/local/etc/rustproxy # configd discovers actions only on startup/reload. A direct `pkg add` must diff --git a/opnsense-plugin/www/rustproxy/src/etc/rc.d/rustproxy b/opnsense-plugin/www/rustproxy/src/etc/rc.d/rustproxy index 3ea5613..8f70e06 100755 --- a/opnsense-plugin/www/rustproxy/src/etc/rc.d/rustproxy +++ b/opnsense-plugin/www/rustproxy/src/etc/rc.d/rustproxy @@ -17,6 +17,7 @@ load_rc_config "$name" : ${rustproxy_data_dir:="/var/db/rustproxy"} : ${rustproxy_log_dir:="/var/log/rustproxy"} : ${rustproxy_log_file:="${rustproxy_log_dir}/rustproxy.log"} +: ${rustproxy_run_dir:="/var/run/rustproxy"} : ${rustproxy_command:="/usr/local/sbin/rust-proxy"} : ${rustproxy_restart_delay:="5"} : ${rustproxy_proxy_listen:="0.0.0.0:8080"} @@ -45,8 +46,8 @@ if [ "$rustproxy_allow_remote_admin" = "1" ]; then rustproxy_runtime_args="${rustproxy_runtime_args} --allow-remote-admin" fi -pidfile="/var/run/rustproxy-daemon.pid" -child_pidfile="/var/run/rustproxy.pid" +pidfile="${rustproxy_run_dir}/daemon.pid" +child_pidfile="${rustproxy_run_dir}/rustproxy.pid" command="/usr/sbin/daemon" command_args="-r -R ${rustproxy_restart_delay} -P ${pidfile} -p ${child_pidfile} -u ${rustproxy_user} -o ${rustproxy_log_file} ${rustproxy_command} ${rustproxy_runtime_args}" required_files="${rustproxy_command}" @@ -55,6 +56,13 @@ stop_postcmd="rustproxy_poststop" rustproxy_prestart() { + # /var/run is ephemeral and recreated on boot. daemon(8) writes the + # child pidfile after dropping privileges to rustproxy, so the runtime + # directory must be writable by the service account. + /usr/bin/install -d -o "${rustproxy_user}" -g "${rustproxy_group}" -m 0750 "${rustproxy_run_dir}" + /usr/sbin/chown "${rustproxy_user}:${rustproxy_group}" "${rustproxy_run_dir}" + /bin/chmod 0750 "${rustproxy_run_dir}" + /usr/bin/install -d -o "${rustproxy_user}" -g "${rustproxy_group}" -m 0700 "${rustproxy_data_dir}" /usr/sbin/chown "${rustproxy_user}:${rustproxy_group}" "${rustproxy_data_dir}" /bin/chmod 0700 "${rustproxy_data_dir}" @@ -67,7 +75,7 @@ rustproxy_prestart() rustproxy_poststop() { - /bin/rm -f "${child_pidfile}" + /bin/rm -f "${child_pidfile}" "${pidfile}" } run_rc_command "$1" diff --git a/pkg/os-rustproxy-latest.pkg b/pkg/os-rustproxy-latest.pkg index b37553c..4a5c07d 100644 Binary files a/pkg/os-rustproxy-latest.pkg and b/pkg/os-rustproxy-latest.pkg differ diff --git a/scripts/check.sh b/scripts/check.sh index 53c58f3..e62f499 100755 --- a/scripts/check.sh +++ b/scripts/check.sh @@ -47,9 +47,13 @@ grep -q 'x86_64-unknown-freebsd' "$ROOT/builder/docker/Dockerfile" grep -q 'BUILD_MODE=docker-freebsd' "$ROOT/builder.env.example" grep -q 'PACKAGING_WORKDIR=/opt/opn-rustproxy/packaging' "$ROOT/builder.env.example" -if [[ -d "$ROOT/builder/freebsd" ]]; then - echo "Obsolete native FreeBSD builder directory is still present." >&2 - exit 1 -fi +[[ -x "$ROOT/builder/freebsd/apply-patches.sh" ]] +[[ -f "$ROOT/builder/freebsd/patches/0.20.0/0001-opnsense-build.patch" ]] +grep -q 'APPLY_FREEBSD_PATCHES=1' "$ROOT/builder.env.example" +grep -q 'builder/freebsd/apply-patches.sh' "$ROOT/builder/debian/build-docker-release.sh" +grep -q 'rustproxy_run_dir:="/var/run/rustproxy"' "$ROOT/opnsense-plugin/www/rustproxy/src/etc/rc.d/rustproxy" +grep -q 'pidfile="${rustproxy_run_dir}/daemon.pid"' "$ROOT/opnsense-plugin/www/rustproxy/src/etc/rc.d/rustproxy" +grep -q 'child_pidfile="${rustproxy_run_dir}/rustproxy.pid"' "$ROOT/opnsense-plugin/www/rustproxy/src/etc/rc.d/rustproxy" +grep -q '/var/run/rustproxy' "$ROOT/opnsense-plugin/www/rustproxy/+POST_INSTALL.pre" echo "Repository checks passed."