diff --git a/README.md b/README.md index 260b0bb..0126340 100644 --- a/README.md +++ b/README.md @@ -69,9 +69,11 @@ dist/ This version intentionally produces an installable `.pkg`, not a full `pkg repo` catalogue. Installation/update is currently done with `pkg add -f`. +The Linux-side package builder validates the FreeBSD package payload before publishing it: payload paths must be absolute (`/usr/local/...`), no undeclared directory entries are archived, and every payload file must have an identical entry in `+MANIFEST`. + ## OPNsense UI -The first version exposes only **Services -> RustProxy -> Enabled**. RustProxy runs as the unprivileged `rustproxy` user. +The OPNsense page **Services -> RustProxy** exposes enable/disable plus HTTP, HTTPS/TLS and administration listener settings. RustProxy runs as the unprivileged `rustproxy` user. ## Validation @@ -80,3 +82,14 @@ The first version exposes only **Services -> RustProxy -> Enabled**. RustProxy r ``` See `docs/FIRST_RUN.md` for Debian setup. + +## OPNsense listener settings + +The OPNsense page **Services -> RustProxy** exposes the runtime listeners used by the rc.d service: + +- HTTP listen: `0.0.0.0:8080` +- HTTPS/TLS listen: `0.0.0.0:8181` (toggleable) +- Admin listen: `127.0.0.1:9090` +- Allow remote admin: off by default; enable it when binding admin to a non-loopback address such as `0.0.0.0:9090` + +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`. diff --git a/builder/debian/build-docker-release.sh b/builder/debian/build-docker-release.sh index f0dcf66..31e7a4e 100755 --- a/builder/debian/build-docker-release.sh +++ b/builder/debian/build-docker-release.sh @@ -41,7 +41,7 @@ if [[ "$RUN_LINUX_TESTS" == "1" ]]; then fi echo "Cross-compiling RustProxy for x86_64-unknown-freebsd..." -docker run "${run_args[@]}" "$DOCKER_IMAGE" bash -lc ' +docker run "${run_args[@]}" "$DOCKER_IMAGE" bash -c ' set -euo pipefail cargo build --locked --release --target x86_64-unknown-freebsd install -m 0555 /target/x86_64-unknown-freebsd/release/rust-proxy /out/rust-proxy diff --git a/builder/debian/package-freebsd.sh b/builder/debian/package-freebsd.sh index 312e748..0e0c631 100755 --- a/builder/debian/package-freebsd.sh +++ b/builder/debian/package-freebsd.sh @@ -89,7 +89,7 @@ common = { "version": version, "origin": "opnsense/os-rustproxy", "comment": "RustProxy reverse proxy service", - "desc": "RustProxy packaged as an OPNsense plugin. The initial UI exposes Enabled/Disabled only.", + "desc": "RustProxy packaged as an OPNsense plugin with configurable HTTP, HTTPS/TLS and administration listeners.", "maintainer": "local@localhost", "www": "https://git.linuxiarz.pl/gru/RustProxy", "prefix": "/usr/local", @@ -120,11 +120,67 @@ manifest["scripts"] = { PY archive="$work/os-rustproxy.tar" -rm -f "$archive" "$package_file" +payload_list="$work/payload.list" +rm -f "$archive" "$package_file" "$payload_list" + +# FreeBSD pkg expects payload paths to match +MANIFEST exactly. The manifest +# uses absolute paths (/usr/local/...), so the tar members must also be +# absolute. Do not archive directory entries: pkg creates parent directories +# while extracting files, and an undeclared directory entry makes pkg add fail. +( + cd "$stage" + find . \( -type f -o -type l \) -print \ + | LC_ALL=C sort \ + | sed 's#^\./##' > "$payload_list" +) + +[[ -s "$payload_list" ]] || { echo "Package payload is empty" >&2; exit 1; } + tar --format=ustar --owner=0 --group=0 --numeric-owner \ -cf "$archive" -C "$meta" +COMPACT_MANIFEST +MANIFEST -tar --format=ustar --owner=0 --group=0 --numeric-owner \ - -rf "$archive" -C "$stage" usr +tar -P --format=ustar --owner=0 --group=0 --numeric-owner --no-recursion \ + --transform='s,^,/,S' \ + -rf "$archive" -C "$stage" -T "$payload_list" + +# Validate the raw package archive before compression. This catches the exact +# class of errors that pkg(8) reports as "not specified in the manifest". +python3 - "$archive" "$meta/+MANIFEST" <<'PYVERIFY' +import json +from pathlib import Path +import sys +import tarfile + +archive = Path(sys.argv[1]) +manifest = json.loads(Path(sys.argv[2]).read_text(encoding="utf-8")) +expected = set(manifest.get("files", {})) + +with tarfile.open(archive, "r:") as tf: + members = tf.getmembers() + +if [m.name for m in members[:2]] != ["+COMPACT_MANIFEST", "+MANIFEST"]: + raise SystemExit("Package metadata must be the first two tar members") + +payload = members[2:] +directories = [m.name for m in payload if m.isdir()] +if directories: + raise SystemExit(f"Directory entries are not allowed in payload: {directories[:5]}") + +actual = {m.name for m in payload if m.isfile() or m.issym() or m.islnk()} +relative = sorted(name for name in actual if not name.startswith("/")) +if relative: + raise SystemExit(f"Payload contains non-absolute paths: {relative[:5]}") + +missing = sorted(expected - actual) +extra = sorted(actual - expected) +if missing or extra: + raise SystemExit( + "Package payload does not match +MANIFEST; " + f"missing={missing[:5]} extra={extra[:5]}" + ) + +print(f"Verified package payload: {len(actual)} files, manifest paths match") +PYVERIFY + zstd -q -f -19 -T0 "$archive" -o "$package_file" # Convenience stable path for direct deployment. diff --git a/builder/docker/Dockerfile b/builder/docker/Dockerfile index 703bc17..6a2176e 100644 --- a/builder/docker/Dockerfile +++ b/builder/docker/Dockerfile @@ -5,7 +5,7 @@ FROM ubuntu:24.04 AS sysroot ARG FREEBSD_RELEASE=15.1-RELEASE ARG FREEBSD_MIRROR=https://download.freebsd.org/releases/amd64/amd64 RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ - ca-certificates wget xz-utils \ + ca-certificates wget xz-utils \ && rm -rf /var/lib/apt/lists/* WORKDIR /tmp/freebsd RUN set -eux; \ @@ -21,11 +21,16 @@ RUN set -eux; \ # RustProxy requires rust-version = 1.88. FROM rust:1.88-slim-bookworm AS builder RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ - ca-certificates clang lld llvm file git perl pkg-config python3 \ + ca-certificates clang lld llvm file git perl pkg-config python3 \ && rm -rf /var/lib/apt/lists/* COPY --from=sysroot /freebsd-sysroot /opt/freebsd-sysroot -RUN rustup target add x86_64-unknown-freebsd + +ENV RUSTUP_TOOLCHAIN=1.88.0 + +RUN rustup target add \ + --toolchain 1.88.0 \ + x86_64-unknown-freebsd ENV CARGO_TARGET_X86_64_UNKNOWN_FREEBSD_LINKER=clang \ CC_x86_64_unknown_freebsd=clang \ diff --git a/ci/build-release.sh b/ci/build-release.sh index 46f1bc2..fabd30f 100755 --- a/ci/build-release.sh +++ b/ci/build-release.sh @@ -4,8 +4,11 @@ set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" CONFIG_FILE="${RUSTPROXY_BUILDER_CONFIG:-/etc/rustproxy-opnsense-builder.env}" if [[ -r "$CONFIG_FILE" ]]; then + # Export all builder configuration to child build scripts. + set -a # shellcheck disable=SC1090 source "$CONFIG_FILE" + set +a fi : "${BUILD_MODE:=docker-freebsd}" diff --git a/docs/FIRST_RUN.md b/docs/FIRST_RUN.md index 7778fee..33226e3 100644 --- a/docs/FIRST_RUN.md +++ b/docs/FIRST_RUN.md @@ -87,6 +87,8 @@ file dist/docker-build/rust-proxy zstd -dc dist/os-rustproxy-latest.pkg | tar -tf - | head -50 ``` +The package payload should contain files such as `/usr/local/sbin/rust-proxy` and must not contain directory entries such as `/usr/` or `/usr/local/`. The build fails automatically when payload paths differ from `+MANIFEST`. + The binary should identify as an x86-64 FreeBSD ELF executable, not a Linux executable. ## 7. Install on a test OPNsense @@ -136,3 +138,19 @@ AUTO_DEPLOY_SSH_KEY=/root/.ssh/opnsense_test ``` Leave both empty for manual deployment. + +## OPNsense runtime settings + +After installation open **Services -> RustProxy**. Defaults are: + +- HTTP listen: `0.0.0.0:8080` +- HTTPS/TLS enabled: yes +- HTTPS/TLS listen: `0.0.0.0:8181` +- Admin listen: `127.0.0.1:9090` +- Allow remote admin: no + +If you want the RustProxy administration UI reachable from another host, set the admin listener to an address such as `0.0.0.0:9090` and enable **Allow remote admin**. Apply restarts/reloads the service with the corresponding RustProxy CLI flags. + +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. diff --git a/opnsense-plugin/www/rustproxy/+POST_INSTALL.pre b/opnsense-plugin/www/rustproxy/+POST_INSTALL.pre old mode 100644 new mode 100755 index c26c8ca..d33354b --- a/opnsense-plugin/www/rustproxy/+POST_INSTALL.pre +++ b/opnsense-plugin/www/rustproxy/+POST_INSTALL.pre @@ -8,6 +8,16 @@ if ! /usr/sbin/pw usershow rustproxy >/dev/null 2>&1; then /usr/sbin/pw useradd rustproxy -g rustproxy -c "RustProxy daemon" -d /var/db/rustproxy -s /usr/sbin/nologin fi -/usr/bin/install -d -o rustproxy -g rustproxy -m 0750 /var/db/rustproxy +/usr/bin/install -d -o rustproxy -g rustproxy -m 0700 /var/db/rustproxy +/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 root -g wheel -m 0755 /usr/local/etc/rustproxy + +# configd discovers actions only on startup/reload. A direct `pkg add` must +# therefore refresh configd before the MVC service controller can call them. +/usr/sbin/service configd restart >/dev/null 2>&1 || true +/usr/local/sbin/pluginctl -cq cache_flush >/dev/null 2>&1 || true +/usr/local/sbin/configctl template reload OPNsense/RustProxy >/dev/null 2>&1 || true + +exit 0 diff --git a/opnsense-plugin/www/rustproxy/pkg-descr b/opnsense-plugin/www/rustproxy/pkg-descr index e47c0e8..5b5fc36 100644 --- a/opnsense-plugin/www/rustproxy/pkg-descr +++ b/opnsense-plugin/www/rustproxy/pkg-descr @@ -1,4 +1,4 @@ -RustProxy packaged as an OPNsense plugin. +RustProxy packaged as an OPNsense service plugin. -The initial OPNsense UI intentionally exposes only one setting: Enabled. -Runtime proxy configuration remains managed by RustProxy itself. +The OPNsense UI exposes service enable/disable, HTTP and HTTPS/TLS listener +addresses, the administration listener and the remote-admin opt-in. diff --git a/opnsense-plugin/www/rustproxy/src/etc/rc.d/rustproxy b/opnsense-plugin/www/rustproxy/src/etc/rc.d/rustproxy index 202f0d3..048581f 100755 --- a/opnsense-plugin/www/rustproxy/src/etc/rc.d/rustproxy +++ b/opnsense-plugin/www/rustproxy/src/etc/rc.d/rustproxy @@ -19,18 +19,46 @@ load_rc_config "$name" : ${rustproxy_log_file:="${rustproxy_log_dir}/rustproxy.log"} : ${rustproxy_command:="/usr/local/sbin/rust-proxy"} : ${rustproxy_restart_delay:="5"} +: ${rustproxy_proxy_listen:="0.0.0.0:8080"} +: ${rustproxy_tls_enabled:="1"} +: ${rustproxy_tls_listen:="0.0.0.0:8181"} +: ${rustproxy_admin_listen:="127.0.0.1:9090"} +: ${rustproxy_allow_remote_admin:="0"} + +enabled="0" +config_file="/usr/local/etc/rustproxy/opnsense.conf" +if [ -r "$config_file" ]; then + . "$config_file" +fi + +if [ "$enabled" = "1" ]; then + rustproxy_enable="YES" +else + rustproxy_enable="NO" +fi + +rustproxy_runtime_args="--data-dir ${rustproxy_data_dir} --admin-listen ${rustproxy_admin_listen} --update-check-interval-secs 0 serve --proxy-listen ${rustproxy_proxy_listen}" +if [ "$rustproxy_tls_enabled" = "1" ]; then + rustproxy_runtime_args="${rustproxy_runtime_args} --tls-listen ${rustproxy_tls_listen}" +fi +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" 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} --data-dir ${rustproxy_data_dir} --update-check-interval-secs 0 serve" +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}" start_precmd="rustproxy_prestart" stop_postcmd="rustproxy_poststop" rustproxy_prestart() { - /usr/bin/install -d -o "${rustproxy_user}" -g "${rustproxy_group}" -m 0750 "${rustproxy_data_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}" + /usr/bin/install -d -o "${rustproxy_user}" -g "${rustproxy_group}" -m 0750 "${rustproxy_log_dir}" /usr/bin/touch "${rustproxy_log_file}" /usr/sbin/chown "${rustproxy_user}:${rustproxy_group}" "${rustproxy_log_file}" diff --git a/opnsense-plugin/www/rustproxy/src/opnsense/mvc/app/controllers/OPNsense/RustProxy/forms/general.xml b/opnsense-plugin/www/rustproxy/src/opnsense/mvc/app/controllers/OPNsense/RustProxy/forms/general.xml index de227d5..8feb8ad 100644 --- a/opnsense-plugin/www/rustproxy/src/opnsense/mvc/app/controllers/OPNsense/RustProxy/forms/general.xml +++ b/opnsense-plugin/www/rustproxy/src/opnsense/mvc/app/controllers/OPNsense/RustProxy/forms/general.xml @@ -1,8 +1,53 @@
+ + + header + true + rustproxy.general.enabled checkbox Start RustProxy and keep it enabled after OPNsense restarts. + + + header + true + + + rustproxy.general.proxyListen + + text + HTTP listener passed as --proxy-listen. Default: 0.0.0.0:8080. + + + rustproxy.general.tlsEnabled + + checkbox + When enabled, pass --tls-listen to RustProxy. + + + rustproxy.general.tlsListen + + text + HTTPS/TLS listener passed as --tls-listen. Default: 0.0.0.0:8181. + + + + header + true + + + rustproxy.general.adminListen + + text + RustProxy administration listener passed as --admin-listen. Default: 127.0.0.1:9090. + + + rustproxy.general.allowRemoteAdmin + + checkbox + Required when Admin listen is not a loopback address, for example 0.0.0.0:9090. This passes --allow-remote-admin. +
diff --git a/opnsense-plugin/www/rustproxy/src/opnsense/mvc/app/models/OPNsense/RustProxy/RustProxy.xml b/opnsense-plugin/www/rustproxy/src/opnsense/mvc/app/models/OPNsense/RustProxy/RustProxy.xml index 7de4fdd..5435f2d 100644 --- a/opnsense-plugin/www/rustproxy/src/opnsense/mvc/app/models/OPNsense/RustProxy/RustProxy.xml +++ b/opnsense-plugin/www/rustproxy/src/opnsense/mvc/app/models/OPNsense/RustProxy/RustProxy.xml @@ -1,13 +1,36 @@ //OPNsense/rustproxy - 1.0.0 + 1.1.0 RustProxy service settings - 0 + 0 Y + + 0.0.0.0:8080 + Y + Enter a valid HTTP listen address and port, for example 0.0.0.0:8080. + + + 1 + Y + + + 0.0.0.0:8181 + Y + Enter a valid HTTPS/TLS listen address and port, for example 0.0.0.0:8181. + + + 127.0.0.1:9090 + Y + Enter a valid administration listen address and port, for example 127.0.0.1:9090. + + + 0 + Y + diff --git a/opnsense-plugin/www/rustproxy/src/opnsense/service/conf/actions.d/actions_rustproxy.conf b/opnsense-plugin/www/rustproxy/src/opnsense/service/conf/actions.d/actions_rustproxy.conf index f3754fd..2d4f657 100644 --- a/opnsense-plugin/www/rustproxy/src/opnsense/service/conf/actions.d/actions_rustproxy.conf +++ b/opnsense-plugin/www/rustproxy/src/opnsense/service/conf/actions.d/actions_rustproxy.conf @@ -3,33 +3,40 @@ command:/usr/local/opnsense/scripts/rustproxy/service.sh start parameters: type:script message:starting RustProxy +allowed_groups:wheel,wwwonly [stop] command:/usr/local/opnsense/scripts/rustproxy/service.sh stop parameters: type:script message:stopping RustProxy +allowed_groups:wheel,wwwonly [restart] command:/usr/local/opnsense/scripts/rustproxy/service.sh restart parameters: type:script message:restarting RustProxy +allowed_groups:wheel,wwwonly [reload] command:/usr/local/opnsense/scripts/rustproxy/service.sh restart parameters: type:script message:reloading RustProxy +allowed_groups:wheel,wwwonly [status] command:/usr/local/opnsense/scripts/rustproxy/service.sh status parameters: type:script_output +errors:no message:RustProxy status +allowed_groups:wheel,wwwonly [boot] command:/usr/local/opnsense/scripts/rustproxy/service.sh boot parameters: type:script message:starting RustProxy at boot +allowed_groups:wheel,wwwonly diff --git a/opnsense-plugin/www/rustproxy/src/opnsense/service/templates/OPNsense/RustProxy/rustproxy.conf b/opnsense-plugin/www/rustproxy/src/opnsense/service/templates/OPNsense/RustProxy/rustproxy.conf index fa00e89..faedc11 100644 --- a/opnsense-plugin/www/rustproxy/src/opnsense/service/templates/OPNsense/RustProxy/rustproxy.conf +++ b/opnsense-plugin/www/rustproxy/src/opnsense/service/templates/OPNsense/RustProxy/rustproxy.conf @@ -1 +1,6 @@ enabled="{% if not helpers.empty('OPNsense.rustproxy.general.enabled') %}1{% else %}0{% endif %}" +rustproxy_proxy_listen="{% if not helpers.empty('OPNsense.rustproxy.general.proxyListen') %}{{ OPNsense.rustproxy.general.proxyListen }}{% else %}0.0.0.0:8080{% endif %}" +rustproxy_tls_enabled="{% if not helpers.empty('OPNsense.rustproxy.general.tlsEnabled') %}1{% else %}0{% endif %}" +rustproxy_tls_listen="{% if not helpers.empty('OPNsense.rustproxy.general.tlsListen') %}{{ OPNsense.rustproxy.general.tlsListen }}{% else %}0.0.0.0:8181{% endif %}" +rustproxy_admin_listen="{% if not helpers.empty('OPNsense.rustproxy.general.adminListen') %}{{ OPNsense.rustproxy.general.adminListen }}{% else %}127.0.0.1:9090{% endif %}" +rustproxy_allow_remote_admin="{% if not helpers.empty('OPNsense.rustproxy.general.allowRemoteAdmin') %}1{% else %}0{% endif %}"