diff --git a/README.md b/README.md index c1f1718..83b2c6d 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,10 @@ Jeden kontener buildera buduje z jednego commita RustProxy: - Linux x86_64: DEB + podpisane repo APT, - Linux x86_64: RPM + podpisane repo DNF/YUM, - FreeBSD x86_64: cross-build bez VM FreeBSD, -- OPNsense: `os-rustproxy-_.pkg` + statyczne repo `pkg`. +- OPNsense: `os-rustproxy-_.pkg` + statyczne repo `pkg`, +- **natywne FreeBSD** (opcjonalnie): `rustproxy-_.pkg` + osobny katalog repo `pkg` pod `/freebsd/` (bez zależności OPNsense). -Nie ma Docker-in-Docker, SCP ani osobnego watchera repo OPNsense. FreeBSD sysroot, clang/lld i Rust target są częścią głównego obrazu buildera. OPNsense dostaje osobny publiczny adres. Gałąź Linux i gałąź OPNsense są budowane oraz publikowane niezależnie, więc błąd testów Linux nie blokuje pakietu FreeBSD. +Nie ma Docker-in-Docker, SCP ani osobnego watchera repo OPNsense. FreeBSD sysroot, clang/lld i Rust target są częścią głównego obrazu buildera. OPNsense dostaje osobny publiczny adres. Gałąź Linux, gałąź OPNsense i opcjonalna gałąź natywnego FreeBSD są budowane oraz publikowane niezależnie, więc błąd testów Linux nie blokuje pakietu FreeBSD. ## Szybki start @@ -49,17 +50,20 @@ opn-rustproxy.linuxiarz.pl -> http://builder:18766 wspólny checkout / \ / \ - Linux branch OPNsense branch - cargo test izolowana kopia źródła - cargo build poprawki FreeBSD - DEB + RPM cross-build FreeBSD - APT + DNF os-rustproxy.pkg - | | - v v - publikacja Linux publikacja OPNsense + Linux branch FreeBSD cross-build + cargo test | + cargo build +---+---+ + DEB + RPM | | + APT + DNF OPNsense FreeBSD native* + | | | + v v v + publikacja Linux os-rustproxy rustproxy.pkg + pkg repo pkg repo + + * FreeBSD native: FREEBSD_NATIVE_ENABLED=1 ``` -`cargo test` należy tylko do gałęzi Linux. Jeśli testy nie przejdą, DEB/RPM nie są publikowane, ale builder nadal uruchamia cross-build FreeBSD i może opublikować repo OPNsense. Stan jest śledzony osobno w `last_linux_commit` i `last_opnsense_commit`, więc kolejny cykl ponawia tylko brakującą gałąź. +`cargo test` należy tylko do gałęzi Linux. Jeśli testy nie przejdą, DEB/RPM nie są publikowane, ale builder nadal uruchamia cross-build FreeBSD i może opublikować repo OPNsense. Stan jest śledzony osobno w `last_linux_commit`, `last_opnsense_commit` i (jeśli włączone) `last_freebsd_commit`; kolejny cykl ponawia tylko brakującą gałąź. ## OPNsense repo @@ -124,6 +128,8 @@ APPLY_FREEBSD_PATCHES=1 `OPNSENSE_RETENTION=10` zachowuje ostatnie 10 wersji pakietu dla danej serii/architektury OPNsense. `0` wyłącza usuwanie starych wersji. +`FREEBSD_NATIVE_ENABLED=0` wyłącza natywny pakiet FreeBSD (domyślnie, bez zmian istniejącej instalacji). Włączenie `1` tworzy oddzielny kanał publikacji i dodaje go do cyklicznego buildera. `FREEBSD_NATIVE_RETENTION=10` ogranicza liczbę zachowanych wersji. Repo używa tego samego publicznego hosta/portu co OPNsense, ale pod oddzielnym prefiksem `OPNSENSE_REPOSITORY_URL/freebsd/`. + ## Automatyczny Rust toolchain podczas budowania Builder sprawdza `rust-version` w `Cargo.toml` **pobranego commita** i opcjonalny @@ -203,17 +209,17 @@ configctl template reload OPNsense/RustProxy configctl rustproxy start ``` -Zmiana wtyczki zwiększa tylko `OPNSENSE_PACKAGING_REVISION` do `3`. -Rewizja pakowania Linux (`PACKAGING_REVISION=2`) pozostaje bez zmian. +Obsługa miękkiego przeładowania zwiększa `OPNSENSE_PACKAGING_REVISION` do `4`. +Pakowanie Linux ma `PACKAGING_REVISION=3`, a natywne FreeBSD `FREEBSD_NATIVE_PACKAGING_REVISION=1`. Każdy kanał otrzymuje osobną aktualizację przy zmianie swojej rewizji. ## Rotacja logów RustProxy - **DEB/RPM:** builder zawsze umieszcza `packaging/rustproxy.logrotate` w `/etc/logrotate.d/rustproxy` (nFPM: `config|noreplace`). Logrotate jest zależnością obu pakietów. Reguła obejmuje `/var/log/rustproxy/*.jsonl`, `*.log` i `vhosts/*.jsonl`, z rotacją codzienną lub po przekroczeniu 100 MiB, 14 archiwami, gzip z opóźnioną kompresją, `copytruncate` oraz `su rustproxy rustproxy`. -- **OPNsense/FreeBSD PKG:** pakiet dostarcza `/usr/local/etc/newsyslog.conf.d/rustproxy.conf` dla tych samych ścieżek; `newsyslog` wykonuje rotację codziennie lub przy rozmiarze 100 MiB, przechowuje 14 archiwów, pomija puste pliki i używa gzip z niekompresowanym najnowszym archiwum. Plik `*.log` jest ponownie otwierany przez `daemon(8)` po `SIGHUP` (opcja `-H` w skrypcie rc.d). OPNsense uruchamia usługę jako `root`, dlatego `newsyslog` używa `root:wheel`. +- **OPNsense / natywne FreeBSD PKG:** pakiet dostarcza `/usr/local/etc/newsyslog.conf.d/rustproxy.conf` dla tych samych ścieżek; `newsyslog` wykonuje rotację codziennie lub przy rozmiarze 100 MiB, przechowuje 14 archiwów, pomija puste pliki i używa gzip z niekompresowanym najnowszym archiwum. Plik `*.log` jest ponownie otwierany przez `daemon(8)` po `SIGHUP` (opcja `-H` w skrypcie rc.d). OPNsense uruchamia usługę jako `root` (`root:wheel` w `newsyslog`); natywne FreeBSD działa jako `rustproxy` (`rustproxy:rustproxy` w `newsyslog`). **Uwaga FreeBSD:** `newsyslog` działa przez zmianę nazwy pliku, nie ma `copytruncate`. Pliki JSONL i inne logi otwarte bezpośrednio przez aplikację muszą być przez nią ponownie otwierane po rotacji. W przeciwnym razie proces może pisać do poprzedniego (przeniesionego) pliku; zachowanie nie jest identyczne z Linux. Reguły są instalowane, ale na działającym OPNsense należy zweryfikować sposób otwierania plików przez RustProxy. -Poprzednia zmiana rotacji logów ustawiła `PACKAGING_REVISION=2`. `check-and-build.sh` sprawdza osobno rewizję pakowania Linux i OPNsense; nowa wersja samej wtyczki przebudowuje **tylko OPNsense**, nawet przy niezmienionym SHA aplikacji. Rewizja pakietu PKG zwiększy się automatycznie. W przypadku zmiany rewizji Linux analogicznie odświeżane są DEB/RPM. Konfiguracja w `/etc/logrotate.d/rustproxy` jest oznaczona jako `noreplace`, więc lokalna wersja zmodyfikowana przez administratora może zostać zachowana (zależnie od menedżera pakietów). +Wersja buildera z obsługą reload zwiększa `PACKAGING_REVISION=3` i `OPNSENSE_PACKAGING_REVISION=4`; nowy kanał używa `FREEBSD_NATIVE_PACKAGING_REVISION=1`. `check-and-build.sh` sprawdza osobno rewizje pakowania Linux, OPNsense i FreeBSD native; nowa wersja samej wtyczki przebudowuje **tylko OPNsense**, nawet przy niezmienionym SHA aplikacji. Rewizja pakietu PKG zwiększy się automatycznie. W przypadku zmiany rewizji Linux analogicznie odświeżane są DEB/RPM. Konfiguracja w `/etc/logrotate.d/rustproxy` jest oznaczona jako `noreplace`, więc lokalna wersja zmodyfikowana przez administratora może zostać zachowana (zależnie od menedżera pakietów). Kontrola po zainstalowaniu: @@ -245,16 +251,17 @@ data/ │ ├── apt -> current/apt │ ├── rpm -> current/rpm │ ├── opnsense -> current/opnsense +│ ├── freebsd -> current/freebsd │ └── releases/ ├── state/ └── work/ ``` -Każda publikacja tworzy nowy snapshot `repository/releases/...` i atomowo przełącza `current`. Przy publikacji Linux poprzedni katalog OPNsense jest zachowywany, a przy publikacji OPNsense zachowywane są aktualne APT/RPM. Dzięki temu obie gałęzie są niezależne, ale nginx nadal serwuje jeden spójny snapshot katalogu. +Każda publikacja tworzy nowy snapshot `repository/releases/...` i atomowo przełącza `current`. Przy publikacji jednego kanału poprzednie katalogi pozostałych kanałów są zachowywane. Dzięki temu wszystkie aktywne kanały są niezależne, ale nginx nadal serwuje jeden spójny snapshot katalogu. ## HTTP i logi -Jeden kontener nginx obsługuje oba repozytoria na dwóch lokalnych portach: +Jeden kontener nginx obsługuje repo Linux i repo OPNsense na dwóch lokalnych portach; natywne FreeBSD jest serwowane jako `/freebsd/` na porcie OPNsense: ```bash docker compose logs -f rust-package-builder @@ -292,7 +299,7 @@ The OPNsense branch reuses the Cargo package `VERSION` already detected by the m ### Czyszczenie cache Cargo -Po **udanym opublikowaniu pakietów** każdej gałęzi (Linux/OPNsense) builder +Po **udanym opublikowaniu pakietów** każdej gałęzi (Linux/OPNsense/FreeBSD native) builder usuwa `target/**/incremental` i `target/debug`, a także pliki starsze niż 30 dni z `target/release/deps` oraz `target/x86_64-unknown-freebsd/release/deps`. Zachowuje aktualne artefakty @@ -302,3 +309,59 @@ od zera. Starsze usunięte zależności mogą wymagać pojedynczej rekompilacji. Ustawienia środowiskowe: `CLEAN_TARGET_AFTER_BUILD=0` wyłącza czyszczenie; `TARGET_CACHE_MAX_AGE_DAYS=30` określa wiek nieużywanych plików. + +## RustProxy 0.34.x: plik `--config` i miękki reload + +- **Linux DEB/RPM:** gdy usługa systemd nie ma własnego `ExecReload`, builder dodaje wywołanie `rust-proxy --data-dir /var/lib/rustproxy --admin-listen 127.0.0.1:9090 reload`. `systemctl reload rustproxy` wykonuje przeładowanie przez administracyjne API, nie restartuje procesu. Usługi z `ProtectSystem=strict` otrzymują dodatkowo zapis do `-/etc/rustproxy`. Aby panel mógł zapisywać wskazany `--config`, plik i jego katalog muszą być własnością użytkownika usługi. Przykładowo można ustawić `RUST_PROXY_CONFIG=/etc/rustproxy/config.json` w `/etc/rustproxy/environment`, po uprzednim utworzeniu poprawnego pliku JSON. +- **OPNsense:** `configctl rustproxy reload` wykonuje miękkie przeładowanie z CLI. Skrypt `service.sh` nie robi już stop/start dla `reload`. Listener GUI po zmianie adresu/portu wymaga nadal `restart`, ponieważ samo przeładowanie nie zmienia gniazd nasłuchujących. Opcjonalna ścieżka `rustproxy_config_path` w `rc.conf` (absolutna, bez spacji) jest przekazywana jako `--config`; panel OPNsense nie ma jeszcze osobnego pola do jej edycji. CLI wymaga osiągalnego adresu administratora na loopback. +- **Czystemu FreeBSD** służy osobna paczka `rustproxy`, bez modeli MVC, configd, syshooków i pluginu OPNsense. Jej natywne `rc.d` uruchamia usługę na koncie `rustproxy`, domyślnie nasłuchuje na HTTP `8080`, admin na `127.0.0.1:9090`, a TLS jest domyślnie wyłączony. Po `sysrc rustproxy_enable=YES` można wykonać `service rustproxy start`, `service rustproxy reload` i `service rustproxy restart`. Ścieżkę JSON można przekazać przez `sysrc rustproxy_config=/usr/local/etc/rustproxy/config.json` po przygotowaniu prawidłowego pliku z uprawnieniami użytkownika `rustproxy`. + +**Ważne:** CLI `reload` wymaga uruchomionego RustProxy, zainicjalizowanego administratora i API osiągalnego przez adres loopback. Zmiana adresów/portów nasłuchujących wymaga `restart`, a nie `reload`. + +## Natywne repo FreeBSD — wersja developerska + +W pliku `.env` ustaw: + +```env +FREEBSD_NATIVE_ENABLED=1 +FREEBSD_NATIVE_RETENTION=10 +``` + +Następnie `./start.sh`. Automatyczna publikacja katalogu `pkg` pojawi się pod: + +```text +${OPNSENSE_REPOSITORY_URL}/freebsd/ + RustProxy-FreeBSD.conf + meta.conf + packagesite.pkg + data.pkg + rustproxy-latest.pkg + FreeBSD:15:amd64/rustproxy-_.pkg +``` + +Na **czystym FreeBSD 15 amd64**: + +```sh +mkdir -p /usr/local/etc/pkg/repos +fetch -o /usr/local/etc/pkg/repos/RustProxy-FreeBSD.conf \ + https://YOUR-OPNSENSE-REPO-HOST/freebsd/RustProxy-FreeBSD.conf +pkg update -f +pkg install rustproxy +sysrc rustproxy_enable=YES +service rustproxy start +service rustproxy reload +``` + +To osobny pakiet od `os-rustproxy`: nie należy instalować obu pakietów naraz, bo oba używają nazwy usługi i ścieżek RustProxy. + +**Bezpieczeństwo repo:** aktualnie katalog `pkg` jest **niesygnowany**, analogicznie do istniejącego repo OPNsense (`signature_type: none`). To nadaje się do testów DEV na zaufanej infrastrukturze. Przed produkcyjnym wdrożeniem należy dodać podpis katalogu `pkg`, bezpieczną dystrybucję klucza publicznego i wykonać instalację oraz aktualizację na rzeczywistym FreeBSD. Generator katalogu i testy offline nie zastępują testu klienta `pkg` na FreeBSD. + +Pakietowe testy offline: + +```sh +./tests/static.sh +./tests/opnsense-plugin.sh +./tests/log-rotation.sh +./tests/freebsd-native.sh +./tests/toolchains.sh +``` diff --git a/docker-compose.yml b/docker-compose.yml index 9fdf0d2..ecced46 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -25,6 +25,8 @@ services: FREEBSD_RUST_TOOLCHAIN: "${FREEBSD_RUST_TOOLCHAIN:-1.88.0}" APPLY_FREEBSD_PATCHES: "${APPLY_FREEBSD_PATCHES:-1}" RUN_LINUX_TESTS: "${RUN_LINUX_TESTS:-1}" + FREEBSD_NATIVE_ENABLED: "${FREEBSD_NATIVE_ENABLED:-0}" + FREEBSD_NATIVE_RETENTION: "${FREEBSD_NATIVE_RETENTION:-10}" volumes: - ./data:/var/lib/package-builder - ./gpg:/run/secrets/gpg:ro diff --git a/http/nginx.conf b/http/nginx.conf index e190752..bf99485 100644 --- a/http/nginx.conf +++ b/http/nginx.conf @@ -88,6 +88,15 @@ http { return 200 "ok\n"; } + # Native FreeBSD package repo at /freebsd/. + # Keep a distinct catalog so clients never see the OPNsense plugin. + location = /freebsd { return 301 /freebsd/; } + location /freebsd/ { + alias /srv/repository/freebsd/; + index index.html; + autoindex off; + } + location = / { try_files /index.html @opnsense_initializing; } diff --git a/opnsense/plugin/+POST_INSTALL.pre b/opnsense/plugin/+POST_INSTALL.pre old mode 100755 new mode 100644 diff --git a/opnsense/plugin/+PRE_DEINSTALL.pre b/opnsense/plugin/+PRE_DEINSTALL.pre old mode 100755 new mode 100644 diff --git a/opnsense/plugin/src/etc/rc.d/rustproxy b/opnsense/plugin/src/etc/rc.d/rustproxy index 31ddb99..c271cae 100755 --- a/opnsense/plugin/src/etc/rc.d/rustproxy +++ b/opnsense/plugin/src/etc/rc.d/rustproxy @@ -18,6 +18,8 @@ load_rc_config "$name" : ${rustproxy_user:="root"} : ${rustproxy_group:="wheel"} : ${rustproxy_data_dir:="/var/db/rustproxy"} +# Optional external canonical JSON (absolute path without spaces). +: ${rustproxy_config_path:=""} : ${rustproxy_log_dir:="/var/log/rustproxy"} : ${rustproxy_log_file:="${rustproxy_log_dir}/rustproxy.log"} : ${rustproxy_run_dir:="/var/run/rustproxy"} @@ -163,7 +165,20 @@ if ! rustproxy_valid_update_interval "$rustproxy_update_check_interval_secs"; th exit 1 fi -rustproxy_runtime_args="--data-dir ${rustproxy_data_dir} --admin-listen ${rustproxy_admin_listen_resolved} --update-check-interval-secs ${rustproxy_update_check_interval_secs} serve --proxy-listen ${rustproxy_proxy_listen_resolved}" +# Restrict to shell-safe absolute paths because rc.subr expands command_args. +rustproxy_config_arg="" +if [ -n "${rustproxy_config_path}" ]; then + case "${rustproxy_config_path}" in + /*) ;; + *) echo "rustproxy_config_path must be an absolute path" >&2; exit 1 ;; + esac + case "${rustproxy_config_path}" in + *[!a-zA-Z0-9_./-]*) echo "rustproxy_config_path contains unsupported characters" >&2; exit 1 ;; + esac + rustproxy_config_arg="--config ${rustproxy_config_path}" +fi + +rustproxy_runtime_args="--data-dir ${rustproxy_data_dir} ${rustproxy_config_arg} --admin-listen ${rustproxy_admin_listen_resolved} --update-check-interval-secs ${rustproxy_update_check_interval_secs} serve --proxy-listen ${rustproxy_proxy_listen_resolved}" if [ "$rustproxy_tls_enabled" = "1" ]; then rustproxy_runtime_args="${rustproxy_runtime_args} --tls-listen ${rustproxy_tls_listen_resolved}" fi @@ -186,6 +201,8 @@ start_precmd="rustproxy_prestart" start_postcmd="rustproxy_poststart" stop_postcmd="rustproxy_poststop" status_cmd="rustproxy_status" +extra_commands="reload" +reload_cmd="rustproxy_reload" rustproxy_prestart() { @@ -286,6 +303,25 @@ rustproxy_status() return 1 } +rustproxy_reload() +{ + # rust-proxy reload authenticates locally via SQLite; it must run as the + # same account as the daemon and address the listener through loopback. + rustproxy_child_pid >/dev/null || { + echo "RustProxy is not running; nothing to reload" >&2 + return 1 + } + _rp_port="${rustproxy_admin_listen_resolved##*:}" + case "${rustproxy_admin_listen_resolved}" in + \[*\]:*) _rp_local_admin="[::1]:${_rp_port}" ;; + *:*) _rp_local_admin="127.0.0.1:${_rp_port}" ;; + *) echo "Invalid RustProxy administration listen address" >&2; return 1 ;; + esac + # The optional path is already checked for safe characters above. + ${rustproxy_command} --data-dir "${rustproxy_data_dir}" ${rustproxy_config_arg} \ + --admin-listen "${_rp_local_admin}" reload +} + rustproxy_poststop() { /bin/rm -f "${child_pidfile}" "${pidfile}" diff --git a/opnsense/plugin/src/opnsense/scripts/rustproxy/service.sh b/opnsense/plugin/src/opnsense/scripts/rustproxy/service.sh index ec9949c..dc81839 100755 --- a/opnsense/plugin/src/opnsense/scripts/rustproxy/service.sh +++ b/opnsense/plugin/src/opnsense/scripts/rustproxy/service.sh @@ -36,10 +36,19 @@ case "${1:-}" in stop) stop_service ;; - restart|reload) + restart) stop_service start_service ;; + reload) + if [ "$enabled" != "1" ]; then + echo "RustProxy is disabled in OPNsense configuration" >&2 + exit 1 + fi + # Soft reload is performed by rust-proxy CLI through the live admin API. + # Listener changes from the GUI still require the explicit restart action. + "$RC_SCRIPT" reload + ;; status) "$RC_SCRIPT" status ;; diff --git a/opnsense/plugin/src/opnsense/service/conf/actions.d/actions_rustproxy.conf b/opnsense/plugin/src/opnsense/service/conf/actions.d/actions_rustproxy.conf index 2d4f657..a8bf5db 100644 --- a/opnsense/plugin/src/opnsense/service/conf/actions.d/actions_rustproxy.conf +++ b/opnsense/plugin/src/opnsense/service/conf/actions.d/actions_rustproxy.conf @@ -20,7 +20,7 @@ message:restarting RustProxy allowed_groups:wheel,wwwonly [reload] -command:/usr/local/opnsense/scripts/rustproxy/service.sh restart +command:/usr/local/opnsense/scripts/rustproxy/service.sh reload parameters: type:script message:reloading RustProxy diff --git a/packaging/freebsd/+POST_INSTALL b/packaging/freebsd/+POST_INSTALL new file mode 100644 index 0000000..bf6b63c --- /dev/null +++ b/packaging/freebsd/+POST_INSTALL @@ -0,0 +1,19 @@ +#!/bin/sh +set -e +# Do not start the service by default on fresh installation. +if ! /usr/sbin/pw groupshow rustproxy >/dev/null 2>&1; then + /usr/sbin/pw groupadd rustproxy +fi +if ! /usr/sbin/pw usershow rustproxy >/dev/null 2>&1; then + /usr/sbin/pw useradd rustproxy -g rustproxy -c 'RustProxy reverse proxy' -d /var/db/rustproxy -s /usr/sbin/nologin +fi +/usr/bin/install -d -m 0700 -o rustproxy -g rustproxy /var/db/rustproxy +/usr/bin/install -d -m 0750 -o rustproxy -g rustproxy /var/log/rustproxy +/usr/bin/install -d -m 0700 -o rustproxy -g rustproxy /usr/local/etc/rustproxy +# On upgrades pkg executes the former pre-deinstall hook first. +if /usr/sbin/service rustproxy enabled >/dev/null 2>&1; then + /usr/sbin/service rustproxy start || echo 'Warning: RustProxy requires attention; see /var/log/rustproxy/rustproxy.log' >&2 +else + echo 'Enable RustProxy with: sysrc rustproxy_enable=YES && service rustproxy start' +fi +exit 0 diff --git a/packaging/freebsd/+PRE_DEINSTALL b/packaging/freebsd/+PRE_DEINSTALL new file mode 100644 index 0000000..fb567de --- /dev/null +++ b/packaging/freebsd/+PRE_DEINSTALL @@ -0,0 +1,4 @@ +#!/bin/sh +# Also stop on upgrade, before the old executable is replaced. +/usr/sbin/service rustproxy onestop >/dev/null 2>&1 || true +exit 0 diff --git a/packaging/freebsd/rustproxy.newsyslog b/packaging/freebsd/rustproxy.newsyslog new file mode 100644 index 0000000..29f123a --- /dev/null +++ b/packaging/freebsd/rustproxy.newsyslog @@ -0,0 +1,5 @@ +# Native FreeBSD RustProxy logs; daemon(8) -H reopens supervised stdout. +# For application-written JSONL, verify that the writer reopens rotated files. +/var/log/rustproxy/*.jsonl rustproxy:rustproxy 0640 14 100M $D0 GBEZPN +/var/log/rustproxy/*.log rustproxy:rustproxy 0640 14 100M $D0 GBEZP /var/run/rustproxy/daemon.pid SIGHUP +/var/log/rustproxy/vhosts/*.jsonl rustproxy:rustproxy 0640 14 100M $D0 GBEZPN diff --git a/packaging/freebsd/rustproxy.rc b/packaging/freebsd/rustproxy.rc new file mode 100644 index 0000000..d8f5ee1 --- /dev/null +++ b/packaging/freebsd/rustproxy.rc @@ -0,0 +1,135 @@ +#!/bin/sh +# PROVIDE: rustproxy +# REQUIRE: LOGIN NETWORKING +# KEYWORD: shutdown + +. /etc/rc.subr +name=rustproxy +rcvar=rustproxy_enable +load_rc_config "$name" + +: ${rustproxy_enable:="NO"} +: ${rustproxy_user:="rustproxy"} +: ${rustproxy_group:="rustproxy"} +: ${rustproxy_data_dir:="/var/db/rustproxy"} +: ${rustproxy_log_dir:="/var/log/rustproxy"} +: ${rustproxy_run_dir:="/var/run/rustproxy"} +: ${rustproxy_config:=""} +: ${rustproxy_proxy_listen:="0.0.0.0:8080"} +: ${rustproxy_tls_listen:=""} +: ${rustproxy_admin_listen:="127.0.0.1:9090"} +: ${rustproxy_allow_remote_admin:="NO"} +: ${rustproxy_update_check_interval_secs:="3600"} + +# rc.subr expands command_args; permit only simple shell-safe paths and +# IPv4 socket addresses to avoid command injection from rc.conf settings. +rustproxy_safe_path() +{ + case "$1" in + /*) ;; + *) echo "RustProxy requires absolute paths: $1" >&2; return 1 ;; + esac + case "$1" in + *[!a-zA-Z0-9_./-]*) echo "Unsupported path characters: $1" >&2; return 1 ;; + esac +} +rustproxy_safe_listen() +{ + case "$1" in + *[!0-9.:]*|''|*::*) echo "Only IPv4 host:port listeners are supported by this rc.d script: $1" >&2; return 1 ;; + *:*) ;; + *) echo "Missing listener port: $1" >&2; return 1 ;; + esac +} +case "${rustproxy_user}:${rustproxy_group}" in + *[!a-zA-Z0-9_:-]*) echo 'Unsupported user/group name' >&2; exit 1 ;; +esac +for rp_path in "$rustproxy_data_dir" "$rustproxy_log_dir" "$rustproxy_run_dir"; do + rustproxy_safe_path "$rp_path" || exit 1 +done +if [ -n "$rustproxy_config" ]; then + rustproxy_safe_path "$rustproxy_config" || exit 1 +fi +rustproxy_safe_listen "$rustproxy_proxy_listen" || exit 1 +rustproxy_safe_listen "$rustproxy_admin_listen" || exit 1 +if [ -n "$rustproxy_tls_listen" ]; then + rustproxy_safe_listen "$rustproxy_tls_listen" || exit 1 +fi +case "$rustproxy_update_check_interval_secs" in + ''|*[!0-9]*) echo 'Invalid update interval' >&2; exit 1 ;; +esac + +rustproxy_bin="/usr/local/sbin/rust-proxy" +rustproxy_config_arg="" +if [ -n "$rustproxy_config" ]; then + rustproxy_config_arg="--config ${rustproxy_config}" +fi +rustproxy_args="--data-dir ${rustproxy_data_dir} ${rustproxy_config_arg} --admin-listen ${rustproxy_admin_listen} --update-check-interval-secs ${rustproxy_update_check_interval_secs} serve --proxy-listen ${rustproxy_proxy_listen}" +if [ -n "$rustproxy_tls_listen" ]; then + rustproxy_args="${rustproxy_args} --tls-listen ${rustproxy_tls_listen}" +fi +if checkyesno rustproxy_allow_remote_admin; then + rustproxy_args="${rustproxy_args} --allow-remote-admin" +fi + +pidfile="${rustproxy_run_dir}/daemon.pid" +child_pidfile="${rustproxy_run_dir}/rustproxy.pid" +command="/usr/sbin/daemon" +command_args="-r -R 5 -H -P ${pidfile} -p ${child_pidfile} -u ${rustproxy_user} -o ${rustproxy_log_dir}/rustproxy.log ${rustproxy_bin} ${rustproxy_args}" +required_files="${rustproxy_bin}" +start_precmd="rustproxy_prestart" +stop_postcmd="rustproxy_poststop" +status_cmd="rustproxy_status" +extra_commands="reload" +reload_cmd="rustproxy_reload" + +rustproxy_prestart() +{ + /usr/bin/install -d -m 0750 -o "${rustproxy_user}" -g "${rustproxy_group}" "${rustproxy_run_dir}" + /usr/bin/install -d -m 0700 -o "${rustproxy_user}" -g "${rustproxy_group}" "${rustproxy_data_dir}" + /usr/bin/install -d -m 0750 -o "${rustproxy_user}" -g "${rustproxy_group}" "${rustproxy_log_dir}" + /usr/bin/touch "${rustproxy_log_dir}/rustproxy.log" + /usr/sbin/chown "${rustproxy_user}:${rustproxy_group}" "${rustproxy_log_dir}/rustproxy.log" + /bin/chmod 0640 "${rustproxy_log_dir}/rustproxy.log" +} + +rustproxy_child_pid() +{ + [ -r "$child_pidfile" ] || return 1 + rp_pid=$(/bin/cat "$child_pidfile" 2>/dev/null) + case "$rp_pid" in + ''|*[!0-9]*) return 1 ;; + esac + /bin/kill -0 "$rp_pid" 2>/dev/null || return 1 + printf '%s\n' "$rp_pid" +} + +rustproxy_status() +{ + if rp_pid=$(rustproxy_child_pid); then + echo "rustproxy is running as pid $rp_pid" + return 0 + fi + echo 'rustproxy is not running' + return 1 +} + +rustproxy_reload() +{ + rustproxy_child_pid >/dev/null || { + echo 'RustProxy is not running; cannot reload' >&2 + return 1 + } + # Change to the daemon's UID. Reload creates a short-lived SQLite session. + # A root-owned SQLite WAL would make the next reload/save fail. + rp_port="${rustproxy_admin_listen##*:}" + rp_command="${rustproxy_bin} --data-dir ${rustproxy_data_dir} ${rustproxy_config_arg} --admin-listen 127.0.0.1:${rp_port} reload" + /usr/bin/su -m "${rustproxy_user}" -c "exec ${rp_command}" +} + +rustproxy_poststop() +{ + /bin/rm -f "$child_pidfile" "$pidfile" +} + +run_rc_command "$1" diff --git a/scripts/build-freebsd-release.sh b/scripts/build-freebsd-release.sh new file mode 100755 index 0000000..5800626 --- /dev/null +++ b/scripts/build-freebsd-release.sh @@ -0,0 +1,118 @@ +#!/usr/bin/env bash +set -Eeuo pipefail +source /opt/rust-package-builder/scripts/lib.sh + +: "${SHA:?SHA required}" +: "${SRC:?SRC required}" +: "${OUT:?OUT required}" +: "${VERSION:?VERSION required}" +: "${SOURCE_REVISION:?SOURCE_REVISION required}" + +REPOSITORY="$DATA_DIR/repository" +BRANCH_RELEASE_ID="$(date -u '+%Y%m%dT%H%M%SZ')-${SHORT_SHA}-freebsd" +RELEASE_DIR="$REPOSITORY/releases/$BRANCH_RELEASE_ID" +PUBLISHED=0 +cleanup_freebsd_release() { + if [[ "$PUBLISHED" != "1" && -d "$RELEASE_DIR" ]]; then + rm -rf "$RELEASE_DIR" + fi +} +trap cleanup_freebsd_release EXIT + +FREEBSD_BINARY="$OUT/rust-proxy-freebsd" +if [[ ! -x "$FREEBSD_BINARY" ]]; then + # Reuse the same FreeBSD cross-build as OPNsense if it succeeded first. + /opt/rust-package-builder/scripts/build-opnsense.sh "$SRC" "$FREEBSD_BINARY" "$BINARY_NAME" "$WORK" +fi + +FREEBSD_PACKAGE_REVISION=$SOURCE_REVISION +LAST_FREEBSD_PACKAGE_VERSION="" +if [[ -r "$DATA_DIR/state/last_freebsd_package_version" ]]; then + LAST_FREEBSD_PACKAGE_VERSION=$(cat "$DATA_DIR/state/last_freebsd_package_version") +elif [[ -r "$REPOSITORY/current/freebsd/BUILD-INFO.txt" ]]; then + LAST_FREEBSD_PACKAGE_VERSION=$(sed -n 's/^package_version=//p' "$REPOSITORY/current/freebsd/BUILD-INFO.txt" | head -n1) +fi +if [[ "$LAST_FREEBSD_PACKAGE_VERSION" == "${VERSION}_"* ]]; then + LAST_FREEBSD_REVISION=${LAST_FREEBSD_PACKAGE_VERSION#"${VERSION}_"} + if [[ "$LAST_FREEBSD_REVISION" =~ ^[0-9]+$ ]] && (( LAST_FREEBSD_REVISION >= FREEBSD_PACKAGE_REVISION )); then + FREEBSD_PACKAGE_REVISION=$((LAST_FREEBSD_REVISION + 1)) + fi +fi +if (( FREEBSD_PACKAGE_REVISION == 0 )); then + FREEBSD_PACKAGE_REVISION=1 +fi +FREEBSD_PACKAGE_VERSION="${VERSION}_${FREEBSD_PACKAGE_REVISION}" +FREEBSD_PACKAGE_FILE="$OUT/rustproxy-${FREEBSD_PACKAGE_VERSION}.pkg" +/opt/rust-package-builder/scripts/package-freebsd.sh \ + "$SRC" "$FREEBSD_BINARY" "$FREEBSD_PACKAGE_FILE" "$FREEBSD_PACKAGE_VERSION" + +mkdir -p "$RELEASE_DIR/apt" "$RELEASE_DIR/rpm/x86_64" "$RELEASE_DIR/opnsense" "$RELEASE_DIR/freebsd" +CURRENT_TARGET="" +if [[ -L "$REPOSITORY/current" ]]; then + CURRENT_LINK=$(readlink "$REPOSITORY/current") + CURRENT_TARGET="$REPOSITORY/$CURRENT_LINK" +fi + +log 'staging native FreeBSD repository snapshot' +if [[ -n "$CURRENT_TARGET" ]]; then + for component in apt rpm opnsense freebsd; do + if [[ -d "$CURRENT_TARGET/$component" ]]; then + rsync -a --delete --link-dest="$CURRENT_TARGET/$component"/ "$CURRENT_TARGET/$component"/ "$RELEASE_DIR/$component"/ + fi + done + for copied in repo-key.gpg index.html LINUX-BUILD-INFO.txt; do + if [[ -f "$CURRENT_TARGET/$copied" ]]; then + cp -a "$CURRENT_TARGET/$copied" "$RELEASE_DIR/$copied" + fi + done +fi + +FREEBSD_PACKAGE_DIR="$RELEASE_DIR/freebsd/$FREEBSD_ABI" +mkdir -p "$FREEBSD_PACKAGE_DIR" +cp -f "$FREEBSD_PACKAGE_FILE" "$FREEBSD_PACKAGE_DIR/" +/opt/rust-package-builder/scripts/generate-opnsense-repo.py "$RELEASE_DIR/freebsd" --retention "$FREEBSD_NATIVE_RETENTION" +NATIVE_URL="${OPNSENSE_REPOSITORY_URL%/}/freebsd" +cat > "$RELEASE_DIR/freebsd/RustProxy-FreeBSD.conf" < "$RELEASE_DIR/freebsd/SHA256" +cat > "$RELEASE_DIR/freebsd/BUILD-INFO.txt" </dev/null || fatal 'invalid FreeBSD packagesite.pkg' +bsdtar -xOf "$FREEBSD_PACKAGE_DIR/$(basename "$FREEBSD_PACKAGE_FILE")" +COMPACT_MANIFEST >/dev/null || fatal 'invalid FreeBSD package manifest' + +log 'publishing native FreeBSD repository branch' +rm -f "$REPOSITORY/current.new" +ln -s "releases/$BRANCH_RELEASE_ID" "$REPOSITORY/current.new" +mv -Tf "$REPOSITORY/current.new" "$REPOSITORY/current" +PUBLISHED=1 +printf '%s\n' "$SHA" | atomic_write "$DATA_DIR/state/last_freebsd_commit" +printf '%s\n' "$FREEBSD_PACKAGE_VERSION" | atomic_write "$DATA_DIR/state/last_freebsd_package_version" +printf '%s\n' "$BRANCH_RELEASE_ID" | atomic_write "$DATA_DIR/state/last_freebsd_release" +printf '%s\n' "$BRANCH_RELEASE_ID" | atomic_write "$DATA_DIR/state/last_release" +log "FreeBSD native publication completed: package=$FREEBSD_PACKAGE_VERSION commit=$SHA" diff --git a/scripts/build-linux-release.sh b/scripts/build-linux-release.sh index 171c834..6c1081f 100755 --- a/scripts/build-linux-release.sh +++ b/scripts/build-linux-release.sh @@ -144,7 +144,7 @@ APT_PUBLIC="$DATA_DIR/aptly/public" gpg --batch --verify "$APT_PUBLIC/dists/$REPO_DISTRIBUTION/Release.gpg" "$APT_PUBLIC/dists/$REPO_DISTRIBUTION/Release" >/dev/null 2>&1 \ || fatal "APT Release signature verification failed" -mkdir -p "$RELEASE_DIR/apt" "$RELEASE_DIR/rpm/x86_64" "$RELEASE_DIR/opnsense" +mkdir -p "$RELEASE_DIR/apt" "$RELEASE_DIR/rpm/x86_64" "$RELEASE_DIR/opnsense" "$RELEASE_DIR/freebsd" CURRENT_TARGET="" if [[ -L "$REPOSITORY/current" ]]; then CURRENT_LINK=$(readlink "$REPOSITORY/current") @@ -152,8 +152,12 @@ if [[ -L "$REPOSITORY/current" ]]; then fi log "staging Linux repository snapshot" -if [[ -n "$CURRENT_TARGET" && -d "$CURRENT_TARGET/opnsense" ]]; then - rsync -a --delete --link-dest="$CURRENT_TARGET/opnsense"/ "$CURRENT_TARGET/opnsense"/ "$RELEASE_DIR/opnsense"/ +if [[ -n "$CURRENT_TARGET" ]]; then + for component in opnsense freebsd; do + if [[ -d "$CURRENT_TARGET/$component" ]]; then + rsync -a --delete --link-dest="$CURRENT_TARGET/$component"/ "$CURRENT_TARGET/$component"/ "$RELEASE_DIR/$component"/ + fi + done fi rsync -a --delete "$APT_PUBLIC"/ "$RELEASE_DIR/apt"/ diff --git a/scripts/build-opnsense-release.sh b/scripts/build-opnsense-release.sh index 51dd611..5d5d1c0 100755 --- a/scripts/build-opnsense-release.sh +++ b/scripts/build-opnsense-release.sh @@ -50,7 +50,7 @@ OPNSENSE_PACKAGE_FILE="$OUT/os-rustproxy-${OPNSENSE_PACKAGE_VERSION}.pkg" /opt/rust-package-builder/scripts/package-opnsense.sh \ "$SRC" "$FREEBSD_BINARY" "$OPNSENSE_PACKAGE_FILE" "$OPNSENSE_PACKAGE_VERSION" -mkdir -p "$RELEASE_DIR/apt" "$RELEASE_DIR/rpm/x86_64" "$RELEASE_DIR/opnsense" +mkdir -p "$RELEASE_DIR/apt" "$RELEASE_DIR/rpm/x86_64" "$RELEASE_DIR/opnsense" "$RELEASE_DIR/freebsd" CURRENT_TARGET="" if [[ -L "$REPOSITORY/current" ]]; then CURRENT_LINK=$(readlink "$REPOSITORY/current") @@ -59,7 +59,7 @@ fi log "staging OPNsense repository snapshot" if [[ -n "$CURRENT_TARGET" ]]; then - for component in apt rpm opnsense; do + for component in apt rpm opnsense freebsd; do if [[ -d "$CURRENT_TARGET/$component" ]]; then rsync -a --delete --link-dest="$CURRENT_TARGET/$component"/ "$CURRENT_TARGET/$component"/ "$RELEASE_DIR/$component"/ fi diff --git a/scripts/build-release.sh b/scripts/build-release.sh index 40ad0ca..d810d44 100755 --- a/scripts/build-release.sh +++ b/scripts/build-release.sh @@ -6,10 +6,12 @@ SHA=${1:?commit SHA required} BUILD_REASON=${2:-new-commit} BUILD_LINUX=${3:-1} BUILD_OPNSENSE=${4:-1} +BUILD_FREEBSD=${5:-0} [[ "$BUILD_LINUX" =~ ^[01]$ ]] || fatal "BUILD_LINUX must be 0 or 1" [[ "$BUILD_OPNSENSE" =~ ^[01]$ ]] || fatal "BUILD_OPNSENSE must be 0 or 1" -(( BUILD_LINUX == 1 || BUILD_OPNSENSE == 1 )) || { log "nothing to build for $SHA"; exit 0; } +[[ "$BUILD_FREEBSD" =~ ^[01]$ ]] || fatal "BUILD_FREEBSD must be 0 or 1" +(( BUILD_LINUX == 1 || BUILD_OPNSENSE == 1 || BUILD_FREEBSD == 1 )) || { log "nothing to build for $SHA"; exit 0; } SHORT_SHA=${SHA:0:12} BUILD_ID="$(date -u '+%Y%m%dT%H%M%SZ')-${SHORT_SHA}" @@ -102,10 +104,11 @@ HOMEPAGE=${PACKAGE_HOMEPAGE:-${CARGO_HOMEPAGE:-${CARGO_REPOSITORY:-${CONTROL_HOM export SHA SHORT_SHA BUILD_ID BUILD_REASON SOURCE_REVISION WORK SRC PKG OUT export PACKAGE_NAME VERSION BINARY_NAME DESCRIPTION LICENSE HOMEPAGE MAINTAINER -log "build plan: commit=$SHA linux=$BUILD_LINUX opnsense=$BUILD_OPNSENSE version=$VERSION" +log "build plan: commit=$SHA linux=$BUILD_LINUX opnsense=$BUILD_OPNSENSE freebsd=$BUILD_FREEBSD version=$VERSION" LINUX_OK=1 OPNSENSE_OK=1 +FREEBSD_OK=1 if [[ "$BUILD_LINUX" == "1" ]]; then if /opt/rust-package-builder/scripts/build-linux-release.sh; then @@ -127,15 +130,27 @@ if [[ "$BUILD_OPNSENSE" == "1" ]]; then fi fi +if [[ "$BUILD_FREEBSD" == "1" ]]; then + if /opt/rust-package-builder/scripts/build-freebsd-release.sh; then + log "FreeBSD native branch completed for $SHA" + cleanup_cargo_target + else + FREEBSD_OK=0 + log "ERROR: FreeBSD native branch failed for $SHA" + fi +fi + LINUX_STATE="" OPNSENSE_STATE="" +FREEBSD_STATE="" [[ -r "$DATA_DIR/state/last_linux_commit" ]] && LINUX_STATE=$(cat "$DATA_DIR/state/last_linux_commit") [[ -r "$DATA_DIR/state/last_opnsense_commit" ]] && OPNSENSE_STATE=$(cat "$DATA_DIR/state/last_opnsense_commit") -if [[ "$LINUX_STATE" == "$SHA" && "$OPNSENSE_STATE" == "$SHA" ]]; then +[[ -r "$DATA_DIR/state/last_freebsd_commit" ]] && FREEBSD_STATE=$(cat "$DATA_DIR/state/last_freebsd_commit") +if [[ "$LINUX_STATE" == "$SHA" && "$OPNSENSE_STATE" == "$SHA" && ( "${FREEBSD_NATIVE_ENABLED:-0}" != "1" || "$FREEBSD_STATE" == "$SHA" ) ]]; then printf '%s\n' "$SHA" | atomic_write "$DATA_DIR/state/last_commit" log "all branches complete for $SHA" fi -if [[ "$LINUX_OK" != "1" || "$OPNSENSE_OK" != "1" ]]; then +if [[ "$LINUX_OK" != "1" || "$OPNSENSE_OK" != "1" || "$FREEBSD_OK" != "1" ]]; then exit 1 fi diff --git a/scripts/check-and-build.sh b/scripts/check-and-build.sh index c179ece..5b707fc 100755 --- a/scripts/check-and-build.sh +++ b/scripts/check-and-build.sh @@ -50,12 +50,28 @@ opnsense_release_complete_for_commit() { return 0 } +freebsd_release_complete_for_commit() { + local sha=$1 + local repo="$CURRENT/freebsd" + [[ -d "$CURRENT" ]] || return 1 + for file in meta.conf packagesite.pkg data.pkg RustProxy-FreeBSD.conf BUILD-INFO.txt rustproxy-latest.pkg; do + [[ -s "$repo/$file" ]] || return 1 + done + grep -Fqx "source_commit=$sha" "$repo/BUILD-INFO.txt" || return 1 + grep -Fqx "packaging_revision=$FREEBSD_NATIVE_PACKAGING_REVISION" "$repo/BUILD-INFO.txt" || return 1 + grep -Fqx "freebsd_abi=$FREEBSD_ABI" "$repo/BUILD-INFO.txt" || return 1 + [[ -d "$repo/$FREEBSD_ABI" ]] || return 1 + find "$repo/$FREEBSD_ABI" -maxdepth 1 -type f -name 'rustproxy-*.pkg' -print -quit | grep -q . || return 1 + return 0 +} + log "checking repository" REMOTE_SHA=$(git ls-remote "$SOURCE_REPO" "refs/heads/$SOURCE_BRANCH" | awk 'NR == 1 {print $1}') [[ "$REMOTE_SHA" =~ ^[0-9a-fA-F]{40}$ ]] || fatal "cannot resolve branch $SOURCE_BRANCH from $SOURCE_REPO" BUILD_LINUX=1 BUILD_OPNSENSE=1 +BUILD_FREEBSD=0 if linux_release_complete_for_commit "$REMOTE_SHA"; then BUILD_LINUX=0 # Persist migrated per-branch state when an older installation already has @@ -67,19 +83,20 @@ if opnsense_release_complete_for_commit "$REMOTE_SHA"; then printf '%s\n' "$REMOTE_SHA" | atomic_write "$DATA_DIR/state/last_opnsense_commit" fi -if [[ "$BUILD_LINUX" == "0" && "$BUILD_OPNSENSE" == "0" ]]; then +if [[ "${FREEBSD_NATIVE_ENABLED:-0}" == "1" ]]; then + BUILD_FREEBSD=1 + if freebsd_release_complete_for_commit "$REMOTE_SHA"; then + BUILD_FREEBSD=0 + printf '%s\n' "$REMOTE_SHA" | atomic_write "$DATA_DIR/state/last_freebsd_commit" + fi +fi + +if [[ "$BUILD_LINUX" == "0" && "$BUILD_OPNSENSE" == "0" && "$BUILD_FREEBSD" == "0" ]]; then printf '%s\n' "$REMOTE_SHA" | atomic_write "$DATA_DIR/state/last_commit" - log "no changes: Linux and OPNsense are complete for $REMOTE_SHA" + log "no changes: all enabled repositories are complete for $REMOTE_SHA" exit 0 fi -if [[ "$BUILD_LINUX" == "1" && "$BUILD_OPNSENSE" == "1" ]]; then - REASON="new-or-missing-both" -elif [[ "$BUILD_LINUX" == "1" ]]; then - REASON="repair-linux" -else - REASON="repair-opnsense" -fi - -log "build required: commit=$REMOTE_SHA linux=$BUILD_LINUX opnsense=$BUILD_OPNSENSE reason=$REASON" -/opt/rust-package-builder/scripts/build-release.sh "$REMOTE_SHA" "$REASON" "$BUILD_LINUX" "$BUILD_OPNSENSE" +REASON="missing-or-outdated-channel" +log "build required: commit=$REMOTE_SHA linux=$BUILD_LINUX opnsense=$BUILD_OPNSENSE freebsd=$BUILD_FREEBSD reason=$REASON" +/opt/rust-package-builder/scripts/build-release.sh "$REMOTE_SHA" "$REASON" "$BUILD_LINUX" "$BUILD_OPNSENSE" "$BUILD_FREEBSD" diff --git a/scripts/entrypoint.sh b/scripts/entrypoint.sh index d4196a2..538b5b7 100755 --- a/scripts/entrypoint.sh +++ b/scripts/entrypoint.sh @@ -16,6 +16,8 @@ FREEBSD_ARCH=${FREEBSD_ARCH:-freebsd:15:x86:64} FREEBSD_RUST_TOOLCHAIN=${FREEBSD_RUST_TOOLCHAIN:-1.88.0} APPLY_FREEBSD_PATCHES=${APPLY_FREEBSD_PATCHES:-1} RUN_LINUX_TESTS=${RUN_LINUX_TESTS:-1} +FREEBSD_NATIVE_ENABLED=${FREEBSD_NATIVE_ENABLED:-0} +FREEBSD_NATIVE_RETENTION=${FREEBSD_NATIVE_RETENTION:-10} [[ "$POLL_INTERVAL" =~ ^[0-9]+$ ]] || fatal "POLL_INTERVAL must be an integer" (( POLL_INTERVAL >= 1 )) || fatal "POLL_INTERVAL must be >= 1" @@ -23,6 +25,8 @@ RUN_LINUX_TESTS=${RUN_LINUX_TESTS:-1} [[ "$OPNSENSE_RETENTION" =~ ^[0-9]+$ ]] || fatal "OPNSENSE_RETENTION must be >= 0" [[ "$APPLY_FREEBSD_PATCHES" =~ ^[01]$ ]] || fatal "APPLY_FREEBSD_PATCHES must be 0 or 1" [[ "$RUN_LINUX_TESTS" =~ ^[01]$ ]] || fatal "RUN_LINUX_TESTS must be 0 or 1" +[[ "$FREEBSD_NATIVE_ENABLED" =~ ^[01]$ ]] || fatal "FREEBSD_NATIVE_ENABLED must be 0 or 1" +[[ "$FREEBSD_NATIVE_RETENTION" =~ ^[0-9]+$ ]] || fatal "FREEBSD_NATIVE_RETENTION must be a nonnegative integer" export DATA_DIR=${DATA_DIR:-/var/lib/package-builder} export SOURCE_REPO SOURCE_BRANCH REPOSITORY_URL OPNSENSE_REPOSITORY_URL POLL_INTERVAL @@ -31,7 +35,7 @@ export REPO_COMPONENT=${REPO_COMPONENT:-main} export PACKAGE_RELEASE export OPNSENSE_ABI OPNSENSE_RETENTION export FREEBSD_RELEASE FREEBSD_ABI FREEBSD_ARCH FREEBSD_RUST_TOOLCHAIN -export APPLY_FREEBSD_PATCHES RUN_LINUX_TESTS +export APPLY_FREEBSD_PATCHES RUN_LINUX_TESTS FREEBSD_NATIVE_ENABLED FREEBSD_NATIVE_RETENTION export MAX_GLIBC_VERSION=${MAX_GLIBC_VERSION:-2.34} export GPG_PRIVATE_KEY_FILE=${GPG_PRIVATE_KEY_FILE:-/run/secrets/gpg/private-key.asc} export GPG_PASSPHRASE_FILE=${GPG_PASSPHRASE_FILE:-/run/secrets/gpg/passphrase} @@ -77,7 +81,7 @@ fi /opt/rust-package-builder/scripts/init-storage.sh -log "builder started: branch=$SOURCE_BRANCH interval=${POLL_INTERVAL}s opnsense=$OPNSENSE_ABI freebsd=$FREEBSD_RELEASE" +log "builder started: branch=$SOURCE_BRANCH interval=${POLL_INTERVAL}s opnsense=$OPNSENSE_ABI freebsd=$FREEBSD_RELEASE native_freebsd=$FREEBSD_NATIVE_ENABLED" while true; do if ! /opt/rust-package-builder/scripts/check-and-build.sh; then log "ERROR: one or more build branches failed; any successful branch was published independently" diff --git a/scripts/generate-freebsd-index.sh b/scripts/generate-freebsd-index.sh new file mode 100755 index 0000000..81c4667 --- /dev/null +++ b/scripts/generate-freebsd-index.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +set -Eeuo pipefail +source /opt/rust-package-builder/scripts/lib.sh +OUT=${1:?output file required} +URL="${OPNSENSE_REPOSITORY_URL%/}/freebsd" +export OUT URL FREEBSD_ABI +python3 <<'PY' +import html, os +h=html.escape +url=h(os.environ['URL']); abi=h(os.environ['FREEBSD_ABI']) +page=f''' +RustProxy - native FreeBSD pkg + +

RustProxy for native FreeBSD

+

This repository contains the rustproxy package and an rc.d service, without OPNsense plugin files. ABI: {abi}.

+

Development repository: unsigned pkg catalog. Use only in an environment where you trust the source and transport. Configure signing before production.

+
mkdir -p /usr/local/etc/pkg/repos
+fetch -o /usr/local/etc/pkg/repos/RustProxy-FreeBSD.conf {url}/RustProxy-FreeBSD.conf
+pkg update -f
+pkg install rustproxy
+sysrc rustproxy_enable=YES
+service rustproxy start
+service rustproxy reload
+

To use an external JSON configuration, set rustproxy_config="/usr/local/etc/rustproxy/config.json" in /etc/rc.conf. The file and parent directory must be writable by the service account.

+ +''' +with open(os.environ['OUT'],'w',encoding='utf-8') as f: + f.write(page) +PY diff --git a/scripts/generate-index.sh b/scripts/generate-index.sh index ca50398..1778c4a 100755 --- a/scripts/generate-index.sh +++ b/scripts/generate-index.sh @@ -72,6 +72,7 @@ sudo dnf install {h(package)}

OPNsense / FreeBSD pkg

The OPNsense plugin is published separately because it uses the FreeBSD pkg format.

{h(opn_url)}/

+

Native FreeBSD pkg (if enabled): {h(opn_url)}/freebsd/.

diff --git a/scripts/generate-opnsense-index.sh b/scripts/generate-opnsense-index.sh index 4291ae1..f5d27b3 100755 --- a/scripts/generate-opnsense-index.sh +++ b/scripts/generate-opnsense-index.sh @@ -39,6 +39,10 @@ code{{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}}

OPNsense series: {h(series)}   ABI: {h(abi)}

+

Native FreeBSD (without OPNsense)

+

The separate optional native FreeBSD pkg repository is available at {h(url)}/freebsd/ when FREEBSD_NATIVE_ENABLED=1.

+
+

pkg repository configuration

mkdir -p /usr/local/etc/pkg/repos
 cat > /usr/local/etc/pkg/repos/RustProxy.conf <<'REPO'
diff --git a/scripts/generate-opnsense-repo.py b/scripts/generate-opnsense-repo.py
index 0bcfb65..8f6b1b9 100755
--- a/scripts/generate-opnsense-repo.py
+++ b/scripts/generate-opnsense-repo.py
@@ -11,7 +11,7 @@ from collections import defaultdict
 from pathlib import Path
 
 CATALOG_FILES = {"data.pkg", "packagesite.pkg"}
-LATEST_NAMES = {"latest.pkg", "os-rustproxy-latest.pkg"}
+LATEST_NAMES = {"latest.pkg", "os-rustproxy-latest.pkg", "rustproxy-latest.pkg"}
 
 
 def sha256(path: Path) -> str:
diff --git a/scripts/init-storage.sh b/scripts/init-storage.sh
index 4a8ce3f..b7e7738 100755
--- a/scripts/init-storage.sh
+++ b/scripts/init-storage.sh
@@ -34,7 +34,7 @@ if ! aptly -config="$APTLY_CONF" repo list -raw | grep -Fx package-builder >/dev
         package-builder >/dev/null
 fi
 
-for item in apt rpm opnsense repo-key.gpg index.html LINUX-BUILD-INFO.txt; do
+for item in apt rpm opnsense freebsd repo-key.gpg index.html LINUX-BUILD-INFO.txt; do
     if [[ -e "$REPOSITORY/$item" && ! -L "$REPOSITORY/$item" ]]; then
         fatal "$REPOSITORY/$item exists and is not a symlink; refusing to overwrite"
     fi
@@ -43,6 +43,7 @@ done
 ln -sfn current/apt "$REPOSITORY/apt"
 ln -sfn current/rpm "$REPOSITORY/rpm"
 ln -sfn current/opnsense "$REPOSITORY/opnsense"
+ln -sfn current/freebsd "$REPOSITORY/freebsd"
 ln -sfn current/repo-key.gpg "$REPOSITORY/repo-key.gpg"
 ln -sfn current/index.html "$REPOSITORY/index.html"
 ln -sfn current/LINUX-BUILD-INFO.txt "$REPOSITORY/LINUX-BUILD-INFO.txt"
diff --git a/scripts/lib.sh b/scripts/lib.sh
index 7b764da..f1074c5 100755
--- a/scripts/lib.sh
+++ b/scripts/lib.sh
@@ -2,8 +2,9 @@
 set -Eeuo pipefail
 
 # Increment when package payload/installation semantics change without a source commit.
-PACKAGING_REVISION=2
-OPNSENSE_PACKAGING_REVISION=3
+PACKAGING_REVISION=3
+OPNSENSE_PACKAGING_REVISION=4
+FREEBSD_NATIVE_PACKAGING_REVISION=1
 
 log() {
     printf '%s %s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" "$*"
diff --git a/scripts/package-freebsd.sh b/scripts/package-freebsd.sh
new file mode 100755
index 0000000..6066228
--- /dev/null
+++ b/scripts/package-freebsd.sh
@@ -0,0 +1,103 @@
+#!/usr/bin/env bash
+set -Eeuo pipefail
+BUILDER_ROOT=${BUILDER_ROOT:-/opt/rust-package-builder}
+source "$BUILDER_ROOT/scripts/lib.sh"
+
+SOURCE_DIR=${1:?source path required}
+BINARY=${2:?FreeBSD binary required}
+PACKAGE_FILE=${3:?output package file required}
+PACKAGE_VERSION=${4:?FreeBSD package version required}
+FREEBSD_ABI=${FREEBSD_ABI:-FreeBSD:15:amd64}
+FREEBSD_ARCH=${FREEBSD_ARCH:-freebsd:15:x86:64}
+PACKAGING="$BUILDER_ROOT/packaging/freebsd"
+
+for tool in python3 sha256sum tar zstd file; do
+    require_cmd "$tool"
+done
+[[ -f "$SOURCE_DIR/Cargo.toml" ]] || fatal "missing Cargo.toml: $SOURCE_DIR"
+[[ -f "$BINARY" ]] || fatal "missing FreeBSD binary: $BINARY"
+[[ "$(file -b "$BINARY")" == *FreeBSD* ]] || fatal "refusing non-FreeBSD native package: $BINARY"
+
+WORK=$(mktemp -d "${PACKAGE_FILE}.work.XXXXXX")
+trap 'rm -rf "$WORK"' EXIT
+STAGE="$WORK/stage"
+META="$WORK/meta"
+ARCHIVE="$WORK/rustproxy.tar"
+mkdir -p "$META" "$(dirname "$PACKAGE_FILE")"
+install -D -m 0555 "$BINARY" "$STAGE/usr/local/sbin/rust-proxy"
+install -D -m 0555 "$PACKAGING/rustproxy.rc" "$STAGE/usr/local/etc/rc.d/rustproxy"
+install -D -m 0644 "$PACKAGING/rustproxy.newsyslog" "$STAGE/usr/local/etc/newsyslog.conf.d/rustproxy.conf"
+
+python3 - "$STAGE" "$META" "$PACKAGE_VERSION" "$FREEBSD_ABI" "$FREEBSD_ARCH" \
+    "$PACKAGING/+POST_INSTALL" "$PACKAGING/+PRE_DEINSTALL" <<'PY'
+import hashlib
+import json
+from pathlib import Path
+import sys
+
+stage, meta = map(Path, sys.argv[1:3])
+version, abi, arch = sys.argv[3:6]
+post_install = Path(sys.argv[6]).read_text(encoding='utf-8')
+pre_deinstall = Path(sys.argv[7]).read_text(encoding='utf-8')
+
+files = {}
+flatsize = 0
+for path in sorted(stage.rglob('*')):
+    if path.is_symlink():
+        files['/' + path.relative_to(stage).as_posix()] = '-'
+    elif path.is_file():
+        files['/' + path.relative_to(stage).as_posix()] = hashlib.sha256(path.read_bytes()).hexdigest()
+        flatsize += path.stat().st_size
+common = {
+    'name': 'rustproxy',
+    'version': version,
+    'origin': 'www/rustproxy',
+    'comment': 'RustProxy reverse proxy for native FreeBSD',
+    'desc': 'RustProxy daemon and rc.d service for FreeBSD (no OPNsense plugin or dependency).',
+    'maintainer': 'local@localhost',
+    'www': 'https://git.linuxiarz.pl/gru/RustProxy',
+    'prefix': '/usr/local',
+    'abi': abi,
+    'arch': arch,
+    'flatsize': flatsize,
+    'licenselogic': 'single',
+    'licenses': ['UNKNOWN'],
+    'categories': ['www'],
+    'deps': {},
+    'options': {},
+}
+manifest = dict(common)
+manifest['files'] = files
+manifest['scripts'] = {'post-install': post_install, 'pre-deinstall': pre_deinstall}
+(meta / '+COMPACT_MANIFEST').write_text(json.dumps(common, separators=(',', ':')) + '\n')
+(meta / '+MANIFEST').write_text(json.dumps(manifest, separators=(',', ':')) + '\n')
+PY
+
+(
+    cd "$STAGE"
+    find . \( -type f -o -type l \) -print | LC_ALL=C sort | sed 's#^\./##' > "$WORK/payload.list"
+)
+[[ -s "$WORK/payload.list" ]] || fatal 'FreeBSD package payload is empty'
+tar --format=ustar --owner=0 --group=0 --numeric-owner \
+    -cf "$ARCHIVE" -C "$META" +COMPACT_MANIFEST +MANIFEST
+tar -P --format=ustar --owner=0 --group=0 --numeric-owner --no-recursion \
+    --transform='s,^,/,S' \
+    -rf "$ARCHIVE" -C "$STAGE" -T "$WORK/payload.list"
+python3 - "$ARCHIVE" "$META/+MANIFEST" <<'PY'
+from pathlib import Path
+import json
+import sys
+import tarfile
+pkg = Path(sys.argv[1])
+manifest = json.loads(Path(sys.argv[2]).read_text())
+with tarfile.open(pkg) as tf:
+    members = tf.getmembers()
+assert [m.name for m in members[:2]] == ['+COMPACT_MANIFEST', '+MANIFEST']
+assert all(not m.isdir() for m in members[2:]), 'package contains directory records'
+actual = {m.name for m in members[2:] if m.isfile() or m.issym() or m.islnk()}
+assert actual == set(manifest['files']), 'payload differs from +MANIFEST'
+assert all(name.startswith('/') for name in actual), 'non-absolute native FreeBSD payload path'
+PY
+zstd -q -f -19 -T0 "$ARCHIVE" -o "$PACKAGE_FILE"
+zstd -dc "$PACKAGE_FILE" | tar -tf - >/dev/null
+log "created native FreeBSD package: $PACKAGE_FILE"
diff --git a/scripts/prepare-package.sh b/scripts/prepare-package.sh
index 55a52aa..2e3f64e 100755
--- a/scripts/prepare-package.sh
+++ b/scripts/prepare-package.sh
@@ -69,6 +69,37 @@ WantedBy=multi-user.target
 UNIT
 fi
 
+# Match the 0.34.x CLI. Avoid changing upstream units that already provide
+# their own ExecReload. Linux package revision forces rebuilding old binaries.
+python3 - "$PKG/payload/usr/lib/systemd/system/${PACKAGE_NAME}.service" "$BINARY_NAME" <<'PY_UNIT'
+from pathlib import Path
+import re
+import sys
+path = Path(sys.argv[1])
+name = sys.argv[2]
+text = path.read_text()
+if not re.search(r'^ExecReload=', text, re.M):
+    if not re.search(r'^ExecStart=', text, re.M):
+        raise SystemExit('unit has no ExecStart; cannot attach reload')
+    text = re.sub(
+        r'^(ExecStart=[^\n]*\n)',
+        lambda m: m.group(1) +
+        f'ExecReload=/usr/bin/{name} --data-dir /var/lib/rustproxy --admin-listen 127.0.0.1:9090 reload\n',
+        text,
+        count=1,
+        flags=re.M,
+    )
+if re.search(r'^ProtectSystem=strict\s*$', text, re.M) and not re.search(
+    r'^ReadWritePaths=.*(?:^|[ \t])-?/etc/rustproxy(?:[ \t]|$)', text, re.M
+):
+    text = re.sub(
+        r'^(ProtectSystem=strict\s*\n)',
+        lambda m: m.group(1) + 'ReadWritePaths=-/etc/rustproxy\n',
+        text, count=1, flags=re.M,
+    )
+path.write_text(text)
+PY_UNIT
+
 if [[ -r "$ENV_SRC" ]]; then
     install -D -m 0640 "$ENV_SRC" "$PKG/payload/etc/${PACKAGE_NAME}/environment"
 fi
@@ -104,7 +135,7 @@ if ! getent passwd "$PACKAGE_NAME" >/dev/null 2>&1; then
     adduser --system --ingroup "$PACKAGE_NAME" --home "/var/lib/$PACKAGE_NAME" --no-create-home \\
         --disabled-password --shell /usr/sbin/nologin "$PACKAGE_NAME" >/dev/null
 fi
-install -d -o "$PACKAGE_NAME" -g "$PACKAGE_NAME" -m 0700 "/var/lib/$PACKAGE_NAME" "/var/log/$PACKAGE_NAME"
+install -d -o "$PACKAGE_NAME" -g "$PACKAGE_NAME" -m 0700 "/var/lib/$PACKAGE_NAME" "/var/log/$PACKAGE_NAME" "/etc/$PACKAGE_NAME"
 SCRIPT
 fi
 cat >> "$PKG/scripts/deb-postinstall.sh" <