This commit is contained in:
Mateusz Gruszczyński committed 2026-10-07 09:23:17 +02:00
1 parent cce1081f75
commit 2b8f11ef8c
36 files changed
+1867 -308

No files matched your search

+42 -5
View File
@@ -1,15 +1,36 @@
# syntax=docker/dockerfile:1
# FreeBSD headers and libraries used only as a cross-compilation sysroot.
FROM rockylinux:9 AS freebsd-sysroot
ARG FREEBSD_RELEASE=15.1-RELEASE
ARG FREEBSD_MIRROR=https://download.freebsd.org/releases/amd64/amd64
RUN dnf -y install --allowerasing ca-certificates coreutils curl tar xz \
&& dnf clean all \
&& rm -rf /var/cache/dnf
WORKDIR /tmp/freebsd
RUN set -eux; \
url="${FREEBSD_MIRROR}/${FREEBSD_RELEASE}"; \
curl -fsSL "${url}/base.txz" -o base.txz; \
curl -fsSL "${url}/MANIFEST" -o MANIFEST; \
expected="$(awk '$1 == "base.txz" { print $2; exit }' MANIFEST)"; \
test -n "$expected"; \
echo "$expected base.txz" | sha256sum -c -; \
mkdir -p /freebsd-sysroot; \
tar -xJf base.txz -C /freebsd-sysroot ./lib ./usr/include ./usr/lib
FROM rockylinux:9
ARG RUST_TOOLCHAIN=stable
ARG FREEBSD_RUST_TOOLCHAIN=1.88.0
ARG NFPM_VERSION=2.47.0
ARG NFPM_RPM_SHA256=5ad3287a0b1d0c068e556bd84965a9866ab6451ef861506a3c09b8971ca5326f
ARG APTLY_VERSION=1.6.3
ARG APTLY_ZIP_SHA256=535ed24eb706bc25a316b59d709a7e74e10165f2ffab8552097ba7e4816a3e7e
RUN dnf -y install --allowerasing \
bash binutils ca-certificates cmake coreutils createrepo_c curl findutils \
gcc gcc-c++ git gnupg2 gzip make openssl pkgconf-pkg-config python3 python3.11 \
rpm rsync shadow-utils sqlite-devel tar unzip xz bzip2 \
bash binutils ca-certificates clang cmake coreutils createrepo_c curl file findutils \
gcc gcc-c++ git gnupg2 gzip libarchive lld llvm make openssl pkgconf-pkg-config \
python3 python3.11 rpm rsync shadow-utils sqlite-devel tar unzip xz bzip2 zstd \
&& dnf clean all \
&& rm -rf /var/cache/dnf
@@ -40,14 +61,30 @@ RUN set -eux; \
curl --proto '=https' --tlsv1.2 -fsSL https://sh.rustup.rs -o /tmp/rustup-init.sh; \
sh /tmp/rustup-init.sh -y --no-modify-path --profile minimal --default-toolchain "${RUST_TOOLCHAIN}" --component rustfmt --component clippy; \
rm -f /tmp/rustup-init.sh; \
rustup toolchain install "${FREEBSD_RUST_TOOLCHAIN}" --profile minimal; \
rustup target add --toolchain "${FREEBSD_RUST_TOOLCHAIN}" x86_64-unknown-freebsd; \
rustc --version; \
cargo --version
cargo --version; \
rustc "+${FREEBSD_RUST_TOOLCHAIN}" --version
COPY --from=freebsd-sysroot /freebsd-sysroot /opt/freebsd-sysroot
ENV FREEBSD_RUST_TOOLCHAIN=${FREEBSD_RUST_TOOLCHAIN} \
CARGO_TARGET_X86_64_UNKNOWN_FREEBSD_LINKER=clang \
CC_x86_64_unknown_freebsd=clang \
CXX_x86_64_unknown_freebsd=clang++ \
AR_x86_64_unknown_freebsd=llvm-ar \
RANLIB_x86_64_unknown_freebsd=llvm-ranlib \
CFLAGS_x86_64_unknown_freebsd="--target=x86_64-unknown-freebsd15.1 --sysroot=/opt/freebsd-sysroot" \
CXXFLAGS_x86_64_unknown_freebsd="--target=x86_64-unknown-freebsd15.1 --sysroot=/opt/freebsd-sysroot" \
CARGO_TARGET_X86_64_UNKNOWN_FREEBSD_RUSTFLAGS="-C link-arg=--target=x86_64-unknown-freebsd15.1 -C link-arg=--sysroot=/opt/freebsd-sysroot -C link-arg=-fuse-ld=lld"
# Registry/git/target cache is persistent, while rustup/toolchains stay in the image.
ENV CARGO_HOME=/var/lib/package-builder/cargo
COPY scripts/ /opt/rust-package-builder/scripts/
RUN chmod 0755 /opt/rust-package-builder/scripts/*.sh
COPY opnsense/ /opt/rust-package-builder/opnsense/
RUN chmod 0755 /opt/rust-package-builder/scripts/*.sh /opt/rust-package-builder/scripts/*.py
VOLUME ["/var/lib/package-builder"]
ENTRYPOINT ["/opt/rust-package-builder/scripts/entrypoint.sh"]
+124 -161
View File
@@ -1,160 +1,159 @@
# Rust Package Builder
# RustProxy unified package builder
Minimalny automat Docker, który cyklicznie sprawdza branch Git, buduje projekt Rust i publikuje podpisane repozytoria APT oraz RPM/DNF. Projekt zawiera również mały, utwardzony serwer HTTP udostępniający gotowe repozytorium i stronę z instrukcją instalacji.
Jeden kontener buildera buduje z jednego commita RustProxy:
## Obsługiwane cele
- 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-<version>_<revision>.pkg` + statyczne repo `pkg`.
- architektura: amd64 / x86_64
- Debian 12, Debian 13
- Ubuntu 24.04, Ubuntu 26.04
- RHEL/Rocky/Alma 9 i 10
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, ale wszystkie artefakty są publikowane jednym atomowym przełączeniem `repository/current`.
Obraz buildera bazuje na Rocky Linux 9. Dzięki temu binarka jest budowana na glibc 2.34; builder dodatkowo odrzuca wynik wymagający nowszego symbolu glibc. Ta sama binarka może być pakowana do DEB i RPM dla wymienionych, nowszych systemów x86_64.
## RustProxy
Dla dostarczonego RustProxy builder automatycznie używa plików znajdujących się w repozytorium źródłowym:
- `debian/rustproxy.service`
- `debian/rustproxy.environment`
- `debian/rustproxy.logrotate`
- `debian/rustproxy.tmpfiles`
- logiki `debian/rustproxy.postinst`
- `grafana/rustproxy-overview.json`
Nie utrzymuje drugiej kopii konfiguracji systemd RustProxy. Wersja i nazwa binarki pochodzą z metadanych Cargo, a nazwa pakietu i maintainer są przejmowane z `debian/control`, jeśli plik istnieje.
## Uruchomienie
Ustawiasz ręcznie tylko dwie wartości:
## Szybki start
```bash
cp .env.example .env
nano .env
./start.sh
```
Przykład dla serwera `192.168.1.100`:
Minimalnie ustaw:
```env
SOURCE_REPO=https://git.linuxiarz.pl/gru/RustProxy.git
REPOSITORY_URL=http://192.168.1.100
OPNSENSE_REPOSITORY_URL=https://opn-rustproxy.linuxiarz.pl
```
Następnie:
Dla `http://` bez portu `start.sh` dopisze domyślny port. Dla `https://` zakładany jest zewnętrzny reverse proxy/TLS.
```bash
./start.sh
```
Jeżeli w `REPOSITORY_URL` nie podasz portu, `start.sh` automatycznie ustawi `18765` i zapisze efektywny adres, np.:
Domyślne lokalne porty:
```text
http://192.168.1.100:18765
APT/RPM: 18765 -> nginx :8080
OPNsense: 18766 -> nginx :8081
```
Jeżeli chcesz inny port, wystarczy wpisać go bezpośrednio w adresie:
```env
REPOSITORY_URL=http://192.168.1.100:23456
```
Nie trzeba ustawiać osobnego `HTTP_PORT` ręcznie — skrypt pobierze go z adresu. `HTTP_PORT` jest zapisywany pomocniczo do `.env`, aby Compose zachował tę samą konfigurację.
`start.sh` automatycznie:
- sprawdza Docker i Docker Compose,
- tworzy katalogi `data/` i `gpg/`,
- buduje obraz buildera,
- przy pierwszym uruchomieniu generuje trwały klucz GPG RSA 3072 bez hasła,
- zapisuje klucz prywatny jako `gpg/private-key.asc`, publiczny jako `gpg/public-key.asc` oraz fingerprint jako `gpg/fingerprint`,
- przy kolejnych uruchomieniach używa tego samego klucza,
- uruchamia builder i mikroserwer HTTP,
- builder natychmiast sprawdza repozytorium Git i po udanym buildzie publikuje APT oraz RPM/DNF,
- strona główna HTTP automatycznie pokazuje instrukcję instalacji i linki do klucza/metadanych.
Można też uruchomić bez wcześniejszego tworzenia `.env`:
```bash
SOURCE_REPO=https://git.linuxiarz.pl/gru/RustProxy.git \
REPOSITORY_URL=http://192.168.1.100 \
./start.sh
```
## Mikro HTTP
Repozytorium jest wystawiane przez osobny kontener `repository-http` oparty o `nginxinc/nginx-unprivileged:stable-alpine-slim`.
Domyślnie:
Przykładowo reverse proxy może kierować:
```text
http://ADRES_SERWERA:18765/
packages.example.net -> http://builder:18765
opn-rustproxy.linuxiarz.pl -> http://builder:18766
```
Serwer HTTP jest celowo bardzo prosty:
- działa jako użytkownik nieuprzywilejowany,
- kontener ma `cap_drop: ALL`,
- ma `no-new-privileges`,
- root filesystem jest read-only,
- repozytorium jest montowane read-only,
- obsługuje wyłącznie `GET` i `HEAD`,
- nie pozwala na upload,
- nie pokazuje listingów katalogów,
- blokuje pliki ukryte,
- dodaje podstawowe nagłówki bezpieczeństwa,
- posiada `/healthz`,
- logi idą do `docker compose logs`.
Przed pierwszym udanym buildem `/` wyświetla prostą informację, że repozytorium jest inicjalizowane. Po publikacji builder atomowo podmienia stronę na właściwą instrukcję instalacji.
### HTTPS
Wbudowany mikroserwer świadomie obsługuje tylko HTTP. Jeżeli `REPOSITORY_URL` zaczyna się od `https://`, zakładane jest, że przed nim stoi zewnętrzny reverse proxy/TLS (np. RustProxy/nginx/Caddy). Sam builder nadal wystawia lokalny HTTP na porcie `18765` lub `HTTP_PORT`.
Dla kompletnego uruchomienia bez reverse proxy użyj `http://IP_LUB_HOST`.
## Klucz GPG
Klucz jest generowany tylko wtedy, gdy `gpg/private-key.asc` nie istnieje. Nie usuwaj katalogu `gpg/` po publikacji repozytorium: zmiana klucza spowoduje, że klienci z wcześniej zainstalowanym kluczem przestaną ufać nowym podpisom.
Do backupu zachowaj co najmniej:
## Pipeline
```text
gpg/private-key.asc
gpg/public-key.asc
gpg/fingerprint
data/
Git RustProxy
|
+-> cargo test
+-> Linux cargo build
| +-> DEB
| +-> RPM
|
+-> izolowana kopia źródła
+-> poprawki FreeBSD/OPNsense
+-> x86_64-unknown-freebsd
+-> os-rustproxy.pkg
|
+-> APT metadata
+-> RPM metadata
+-> OPNsense pkg metadata
+-> walidacja
+-> atomic switch repository/current
```
Jeżeli którykolwiek etap się nie powiedzie, publiczne repozytoria nie są przełączane.
## OPNsense repo
Publiczny root `OPNSENSE_REPOSITORY_URL` zawiera m.in.:
```text
/
├── meta.conf
├── packagesite.pkg
├── data.pkg
├── RustProxy.conf
├── BUILD-INFO.txt
├── SHA256
├── os-rustproxy-latest.pkg
└── 26.7/
└── amd64/
├── os-rustproxy-0.20.0_123.pkg
└── ...
```
`RustProxy.conf` jest generowany automatycznie i wskazuje na `OPNSENSE_REPOSITORY_URL`. Repo OPNsense jest obecnie niesygnowane, zgodnie z wcześniejszym builderem/repo (`signature_type: "none"`). Repo APT/RPM pozostaje podpisane GPG.
Ręczna konfiguracja na OPNsense:
```sh
mkdir -p /usr/local/etc/pkg/repos
fetch -o /usr/local/etc/pkg/repos/RustProxy.conf \
https://opn-rustproxy.linuxiarz.pl/RustProxy.conf
pkg update -f
pkg install os-rustproxy
```
## Konfiguracja
Wymagane:
Najważniejsze zmienne:
```text
SOURCE_REPO
REPOSITORY_URL
```
Domyślne / automatyczne:
```text
```env
SOURCE_REPO=https://git.linuxiarz.pl/gru/RustProxy.git
SOURCE_BRANCH=master
POLL_INTERVAL=300
REPOSITORY_URL=http://192.168.1.100
HTTP_PORT=18765
REPO_DISTRIBUTION=stable
REPO_COMPONENT=main
OPNSENSE_REPOSITORY_URL=https://opn-rustproxy.linuxiarz.pl
OPNSENSE_HTTP_PORT=18766
POLL_INTERVAL=300
PACKAGE_RELEASE=auto
MAX_GLIBC_VERSION=2.34
GPG_PRIVATE_KEY_FILE=/run/secrets/gpg/private-key.asc
GPG_PASSPHRASE_FILE=/run/secrets/gpg/passphrase
RUN_LINUX_TESTS=1
OPNSENSE_ABI=26.7
OPNSENSE_RETENTION=10
FREEBSD_RELEASE=15.1-RELEASE
FREEBSD_ABI=FreeBSD:15:amd64
FREEBSD_ARCH=freebsd:15:x86:64
FREEBSD_RUST_TOOLCHAIN=1.88.0
APPLY_FREEBSD_PATCHES=1
```
`GPG_PASSPHRASE_FILE` nie musi istnieć dla automatycznie generowanego klucza bez hasła. `GPG_KEY_ID` może być podany przez ENV; jeśli go brak, builder wybiera pierwszy zaimportowany klucz prywatny.
`OPNSENSE_RETENTION=10` zachowuje ostatnie 10 wersji pakietu dla danej serii/architektury OPNsense. `0` wyłącza usuwanie starych wersji.
## Dane trwałe
## FreeBSD cross-build
Obraz buildera pobiera i weryfikuje `base.txz` dla `FREEBSD_RELEASE`, a następnie używa:
```text
/var/lib/package-builder/
/opt/freebsd-sysroot
clang / lld / llvm
Rust target x86_64-unknown-freebsd
```
Poprawki tylko dla builda OPNsense są w:
```text
opnsense/patches/<wersja-RustProxy>/*.patch
```
Są nakładane na izolowaną kopię źródła. Build Linux nie jest modyfikowany.
Kod pluginu OPNsense znajduje się w:
```text
opnsense/plugin/
```
## Dane
```text
data/
├── aptly/
├── cargo/
├── packages/
@@ -164,69 +163,33 @@ GPG_PASSPHRASE_FILE=/run/secrets/gpg/passphrase
│ ├── current -> releases/<build>
│ ├── apt -> current/apt
│ ├── rpm -> current/rpm
│ ├── repo-key.gpg -> current/repo-key.gpg
│ ├── index.html -> current/index.html
│ ├── opnsense -> current/opnsense
│ └── releases/
├── state/
│ ├── last_commit
│ ├── last_version
│ └── last_package_release
└── work/
```
Poprzednie wersje pakietów nie są usuwane. Aktualne repozytoria APT i RPM zawierają historię pakietów. Przy `PACKAGE_RELEASE=auto` nowy commit z niezmienionym `Cargo.toml version` dostaje kolejną rewizję paczki (`-2`, `-3`, ...), więc nie nadpisuje wcześniej opublikowanego artefaktu.
Jeden release zawiera jednocześnie `apt/`, `rpm/` i `opnsense/`. Dzięki temu oba publiczne adresy przechodzą na nową wersję jednocześnie.
## Publikacja bez uszkodzenia działającego repo
## HTTP i logi
Nowy commit przechodzi kolejno:
```text
clone -> cargo build --release -> nFPM DEB/RPM -> podpisy -> aptly -> createrepo_c -> walidacja -> atomic switch
```
Publiczny symlink `repository/current` jest podmieniany dopiero na końcu. Błąd kompilacji, pakowania, podpisu lub generowania metadata nie przełącza publicznego repozytorium. `last_commit` jest zapisywany dopiero po udanej publikacji, więc nieudany commit zostanie ponowiony w następnym cyklu.
Kolejne snapshoty w `repository/releases/` używają hardlinków do niezmienionych plików poprzedniego snapshotu.
## Podpisy
- DEB: podpis GPG nFPM (`_gpgorigin`) oraz podpisane `Release.gpg`/`InRelease` przez aptly.
- RPM: podpis GPG pakietu nFPM.
- RPM metadata: `repodata/repomd.xml.asc`; instrukcja używa `repo_gpgcheck=1` i `gpgcheck=1`.
- publiczny klucz: `http(s)://.../repo-key.gpg` jako ASCII-armored OpenPGP, zgodny z `gpg --dearmor` i `rpm --import`.
## Logi i kontrola
Jeden kontener nginx obsługuje oba repozytoria na dwóch lokalnych portach:
```bash
docker compose logs -f rust-package-builder
docker compose logs -f repository-http
curl -f http://127.0.0.1:18765/healthz
curl -f http://127.0.0.1:18766/healthz
```
## Test plików dostarczonego RustProxy
## Testy statyczne
```bash
./tests/static.sh
./tests/check-rustproxy-source.sh /sciezka/do/RustProxy
```
## Własny klucz GPG (opcjonalnie)
## Klucz GPG
Jeżeli zamiast automatycznie generowanego klucza chcesz użyć własnego, umieść go przed pierwszym `./start.sh` jako:
```text
gpg/private-key.asc
```
Przykładowy eksport:
```bash
gpg --armor --export-secret-keys YOUR_KEY_ID > gpg/private-key.asc
chmod 600 gpg/private-key.asc
```
`start.sh` wykryje istniejący klucz i nie wygeneruje nowego. Klucz prywatny nie jest kopiowany do obrazu. Kontener otrzymuje katalog `gpg/` wyłącznie jako read-only mount `/run/secrets/gpg`.
## Rocky Linux 9 / pakiety bazowe
Dockerfile używa `dnf --allowerasing`, ponieważ obrazy Rocky Linux 9 mogą zawierać `coreutils-single` i `curl-minimal`. Podczas budowy są one zastępowane pełnymi pakietami `coreutils` i `curl`, wymaganymi przez builder.
`./start.sh` generuje trwały klucz GPG przy pierwszym uruchomieniu. Katalogu `gpg/` nie należy usuwać po publikacji APT/RPM.
+18
View File
@@ -2,12 +2,29 @@ services:
rust-package-builder:
build:
context: .
args:
FREEBSD_RELEASE: "${FREEBSD_RELEASE:-15.1-RELEASE}"
FREEBSD_RUST_TOOLCHAIN: "${FREEBSD_RUST_TOOLCHAIN:-1.88.0}"
image: linuxiarz/rust-package-builder:latest
environment:
SOURCE_REPO: "${SOURCE_REPO:?SOURCE_REPO is required}"
SOURCE_BRANCH: "${SOURCE_BRANCH:-master}"
REPOSITORY_URL: "${REPOSITORY_URL:?REPOSITORY_URL is required}"
OPNSENSE_REPOSITORY_URL: "${OPNSENSE_REPOSITORY_URL:?OPNSENSE_REPOSITORY_URL is required}"
POLL_INTERVAL: "${POLL_INTERVAL:-300}"
REPO_DISTRIBUTION: "${REPO_DISTRIBUTION:-stable}"
REPO_COMPONENT: "${REPO_COMPONENT:-main}"
PACKAGE_RELEASE: "${PACKAGE_RELEASE:-auto}"
MAX_GLIBC_VERSION: "${MAX_GLIBC_VERSION:-2.34}"
KEEP_WORKSPACE: "${KEEP_WORKSPACE:-0}"
OPNSENSE_ABI: "${OPNSENSE_ABI:-26.7}"
OPNSENSE_RETENTION: "${OPNSENSE_RETENTION:-10}"
FREEBSD_RELEASE: "${FREEBSD_RELEASE:-15.1-RELEASE}"
FREEBSD_ABI: "${FREEBSD_ABI:-FreeBSD:15:amd64}"
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}"
volumes:
- ./data:/var/lib/package-builder
- ./gpg:/run/secrets/gpg:ro
@@ -17,6 +34,7 @@ services:
image: nginxinc/nginx-unprivileged:stable-alpine-slim
ports:
- "0.0.0.0:${HTTP_PORT:-18765}:8080"
- "0.0.0.0:${OPNSENSE_HTTP_PORT:-18766}:8081"
volumes:
- ./data/repository:/srv/repository:ro
- ./http/nginx.conf:/etc/nginx/nginx.conf:ro
+46 -5
View File
@@ -23,6 +23,7 @@ http {
uwsgi_temp_path /tmp/uwsgi_temp;
scgi_temp_path /tmp/scgi_temp;
# Linux APT/RPM repository.
server {
listen 8080 default_server;
server_name _;
@@ -34,8 +35,6 @@ http {
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "no-referrer" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
add_header Content-Security-Policy "default-src 'none'; style-src 'unsafe-inline'; img-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'" always;
if ($request_method !~ ^(GET|HEAD)$) {
return 405;
@@ -47,12 +46,54 @@ http {
}
location = / {
try_files /index.html @initializing;
try_files /index.html @linux_initializing;
}
location @initializing {
location @linux_initializing {
default_type text/html;
return 200 '<!doctype html><html lang="pl"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Rust Package Repository</title><style>body{font:16px system-ui,sans-serif;max-width:760px;margin:64px auto;padding:0 20px;color:#1f2937}code{background:#f3f4f6;padding:.15rem .35rem;border-radius:.3rem}</style></head><body><h1>Rust Package Repository</h1><p>Repozytorium HTTP działa. Pierwszy build pakietu jeszcze trwa albo nie został zakończony.</p><p>Stan buildera sprawdzisz przez <code>docker compose logs -f rust-package-builder</code>.</p></body></html>';
return 200 '<!doctype html><html><head><meta charset="utf-8"><title>RustProxy repository</title></head><body><h1>RustProxy Linux repository</h1><p>Repository is being initialized.</p></body></html>';
}
location ~ (^|/)\. {
deny all;
}
location / {
try_files $uri =404;
}
}
# OPNsense/FreeBSD pkg repository. It has a separate public URL and local port,
# but follows the same atomic repository/current switch as the Linux repo.
server {
listen 8081;
server_name _;
root /srv/repository/opnsense;
index index.html;
autoindex off;
#add_header X-Content-Type-Options "nosniff" always;
#add_header X-Frame-Options "DENY" always;
#add_header Referrer-Policy "no-referrer" always;
#add_header Cache-Control "no-cache, no-store, must-revalidate" always;
if ($request_method !~ ^(GET|HEAD)$) {
return 405;
}
location = /healthz {
default_type text/plain;
return 200 "ok\n";
}
location = / {
try_files /index.html @opnsense_initializing;
}
location @opnsense_initializing {
default_type text/html;
return 200 '<!doctype html><html><head><meta charset="utf-8"><title>RustProxy OPNsense repository</title></head><body><h1>RustProxy OPNsense repository</h1><p>Repository is being initialized.</p></body></html>';
}
location ~ (^|/)\. {
@@ -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::<h3::ext::Protocol>().unwrap());
+ .insert(
+ "websocket"
+ .parse::<h3::ext::Protocol>()
+ .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::<h3::ext::Protocol>().unwrap());
+ .insert(
+ "connect-udp"
+ .parse::<h3::ext::Protocol>()
+ .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::<h3::ext::Protocol>().unwrap());
+ .insert(
+ "websocket"
+ .parse::<h3::ext::Protocol>()
+ .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");
+47
View File
@@ -0,0 +1,47 @@
#!/bin/sh
# Keep the historical service account so package downgrades remain possible,
# but current OPNsense runtime executes RustProxy as root (see rc.d script).
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 daemon" -d /var/db/rustproxy -s /usr/sbin/nologin
fi
/usr/bin/install -d -o root -g wheel -m 0700 /var/db/rustproxy
/usr/sbin/chown -R root:wheel /var/db/rustproxy
/bin/chmod 0700 /var/db/rustproxy
/usr/bin/install -d -o root -g wheel -m 0750 /var/log/rustproxy
/usr/bin/install -d -o root -g wheel -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
# 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
# Apply model migrations before rendering the service template. In particular
# 1.4.0 copies the old combined listener values into the split address/port
# fields so upgrades keep their existing ports instead of falling back.
if [ -x /usr/local/opnsense/mvc/script/run_migrations.php ]; then
/usr/local/opnsense/mvc/script/run_migrations.php >/dev/null 2>&1 || true
fi
/usr/local/sbin/configctl template reload OPNsense/RustProxy >/dev/null 2>&1 || true
# A package upgrade invokes the previous package pre-deinstall hook, which stops
# RustProxy before replacing the binary. Start it again after migrations and
# template rendering. Use the installed service helper directly so this does
# not depend on configd having finished reloading its action registry. The
# helper itself checks enabled=1; on a fresh/default-disabled install this is a
# no-op. restart is deliberate: if an old process survived a forced pkg add,
# it is stopped before the new binary starts.
if [ -x /usr/local/opnsense/scripts/rustproxy/service.sh ]; then
if ! /usr/local/opnsense/scripts/rustproxy/service.sh restart; then
echo "WARNING: RustProxy package installed but the service could not be started." >&2
echo "Check /var/log/rustproxy/rustproxy.log and run: configctl rustproxy start" >&2
fi
fi
exit 0
+13
View File
@@ -0,0 +1,13 @@
#!/bin/sh
# pkg upgrade runs the old package pre-deinstall hook before installing the new
# binary. Stop the supervised child first so the upgrade never leaves an old
# RustProxy process running from an unlinked executable. configctl is preferred
# on OPNsense; the rc.d fallback also works while configd is restarting.
if ! /usr/local/sbin/configctl rustproxy stop >/dev/null 2>&1; then
if [ -x /usr/local/etc/rc.d/rustproxy ]; then
/usr/local/etc/rc.d/rustproxy onestop >/dev/null 2>&1 || true
fi
fi
exit 0
+9
View File
@@ -0,0 +1,9 @@
PLUGIN_NAME= rustproxy
PLUGIN_VERSION?= 0.23.3
PLUGIN_REVISION?= 1
PLUGIN_COMMENT= RustProxy reverse proxy service
PLUGIN_MAINTAINER?= local@localhost
PLUGIN_WWW?= https://git.linuxiarz.pl/gru/RustProxy
PLUGIN_LICENSE= UNKNOWN
.include "../../Mk/plugins.mk"
+4
View File
@@ -0,0 +1,4 @@
RustProxy packaged as an OPNsense service plugin.
The OPNsense UI exposes service enable/disable, separate bind-address and
port fields for HTTP, HTTPS/TLS and administration, plus remote-admin opt-in.
+276
View File
@@ -0,0 +1,276 @@
#!/bin/sh
# PROVIDE: rustproxy
# REQUIRE: LOGIN NETWORKING
# KEYWORD: shutdown
. /etc/rc.subr
name="rustproxy"
rcvar="${name}_enable"
load_rc_config "$name"
: ${rustproxy_enable:="NO"}
# RustProxy on OPNsense/FreeBSD runs as root. In practice the application
# needs access to its complete data/certificate state and service startup was
# unreliable after daemon(8) dropped privileges to the dedicated account.
: ${rustproxy_user:="root"}
: ${rustproxy_group:="wheel"}
: ${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_allow_remote_admin:="1"}
# FreeBSD uses jemalloc rather than glibc. Keep the same low-arena intent as
# Linux MALLOC_ARENA_MAX=2 with the native jemalloc knob. rc.conf can override
# rustproxy_malloc_conf or set it to an empty value to disable the tuning.
if [ "${rustproxy_malloc_conf+x}" != "x" ]; then
rustproxy_malloc_conf="${MALLOC_CONF:-narenas:2}"
fi
# Split listener settings written by the current OPNsense model.
: ${rustproxy_proxy_address:=""}
: ${rustproxy_proxy_port:=""}
: ${rustproxy_tls_enabled:="1"}
: ${rustproxy_tls_address:=""}
: ${rustproxy_tls_port:=""}
: ${rustproxy_admin_address:=""}
: ${rustproxy_admin_port:=""}
# Combined listener settings used by plugin model 1.2.x and earlier.
: ${rustproxy_proxy_listen_legacy:=""}
: ${rustproxy_tls_listen_legacy:=""}
: ${rustproxy_admin_listen_legacy:=""}
# Old rc.conf-style variable names are also accepted for hand-written configs.
: ${rustproxy_proxy_listen:=""}
: ${rustproxy_tls_listen:=""}
: ${rustproxy_admin_listen:=""}
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_valid_port()
{
case "$1" in
''|*[!0-9]*) return 1 ;;
esac
[ "$1" -ge 1 ] 2>/dev/null && [ "$1" -le 65535 ] 2>/dev/null
}
rustproxy_valid_listener()
{
_rp_listener="$1"
case "$_rp_listener" in
\[*\]:*) _rp_port="${_rp_listener##*:}" ;;
*:*) _rp_port="${_rp_listener##*:}" ;;
*) return 1 ;;
esac
rustproxy_valid_port "$_rp_port"
}
rustproxy_format_listener()
{
_rp_address="$1"
_rp_port="$2"
case "$_rp_address" in
\[*\]) printf '%s:%s\n' "$_rp_address" "$_rp_port" ;;
*:*) printf '[%s]:%s\n' "$_rp_address" "$_rp_port" ;;
*) printf '%s:%s\n' "$_rp_address" "$_rp_port" ;;
esac
}
rustproxy_resolve_listener()
{
_rp_address="$1"
_rp_port="$2"
_rp_legacy="$3"
_rp_old_rc="$4"
_rp_default_address="$5"
_rp_default_port="$6"
_rp_name="$7"
# New split settings are authoritative as soon as either half is set.
if [ -n "$_rp_address" ] || [ -n "$_rp_port" ]; then
[ -n "$_rp_address" ] || _rp_address="$_rp_default_address"
[ -n "$_rp_port" ] || _rp_port="$_rp_default_port"
if ! rustproxy_valid_port "$_rp_port"; then
echo "Invalid RustProxy ${_rp_name} port: ${_rp_port}" >&2
return 1
fi
rustproxy_format_listener "$_rp_address" "$_rp_port"
return 0
fi
# Preserve a valid listener from plugin 1.2.x during upgrade.
if [ -n "$_rp_legacy" ] && rustproxy_valid_listener "$_rp_legacy"; then
printf '%s\n' "$_rp_legacy"
return 0
fi
# Preserve hand-written rc.conf settings as another compatibility path.
if [ -n "$_rp_old_rc" ] && rustproxy_valid_listener "$_rp_old_rc"; then
printf '%s\n' "$_rp_old_rc"
return 0
fi
# Malformed legacy values such as "0.0.0.0:" must never reach RustProxy.
# Fall back to a known-good listener and let the user set the separate port
# field in Services -> RustProxy.
rustproxy_format_listener "$_rp_default_address" "$_rp_default_port"
}
rustproxy_proxy_listen_resolved="$(rustproxy_resolve_listener \
"$rustproxy_proxy_address" "$rustproxy_proxy_port" \
"$rustproxy_proxy_listen_legacy" "$rustproxy_proxy_listen" \
"0.0.0.0" "8080" "HTTP")" || exit 1
rustproxy_tls_listen_resolved="$(rustproxy_resolve_listener \
"$rustproxy_tls_address" "$rustproxy_tls_port" \
"$rustproxy_tls_listen_legacy" "$rustproxy_tls_listen" \
"0.0.0.0" "8181" "HTTPS/TLS")" || exit 1
rustproxy_admin_listen_resolved="$(rustproxy_resolve_listener \
"$rustproxy_admin_address" "$rustproxy_admin_port" \
"$rustproxy_admin_listen_legacy" "$rustproxy_admin_listen" \
"0.0.0.0" "9090" "admin")" || exit 1
rustproxy_runtime_args="--data-dir ${rustproxy_data_dir} --admin-listen ${rustproxy_admin_listen_resolved} --update-check-interval-secs 0 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
if [ "$rustproxy_allow_remote_admin" = "1" ]; then
rustproxy_runtime_args="${rustproxy_runtime_args} --allow-remote-admin"
fi
pidfile="${rustproxy_run_dir}/daemon.pid"
child_pidfile="${rustproxy_run_dir}/rustproxy.pid"
command="/usr/sbin/daemon"
rustproxy_daemon_user_arg=""
if [ "$rustproxy_user" != "root" ]; then
rustproxy_daemon_user_arg="-u ${rustproxy_user}"
fi
command_args="-r -R ${rustproxy_restart_delay} -P ${pidfile} -p ${child_pidfile} ${rustproxy_daemon_user_arg} -o ${rustproxy_log_file} ${rustproxy_command} ${rustproxy_runtime_args}"
required_files="${rustproxy_command}"
start_precmd="rustproxy_prestart"
start_postcmd="rustproxy_poststart"
stop_postcmd="rustproxy_poststop"
status_cmd="rustproxy_status"
rustproxy_prestart()
{
# RustProxy on OPNsense/FreeBSD uses jemalloc. MALLOC_ARENA_MAX is a
# glibc/Linux setting and has no useful effect here, so export MALLOC_CONF
# instead. daemon(8) inherits this environment and passes it to RustProxy.
if [ -n "${rustproxy_malloc_conf}" ]; then
MALLOC_CONF="${rustproxy_malloc_conf}"
export MALLOC_CONF
else
unset MALLOC_CONF
fi
# /var/run is ephemeral and recreated on boot.
/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}"
/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}"
/bin/chmod 0640 "${rustproxy_log_file}"
echo "RustProxy listeners: HTTP=${rustproxy_proxy_listen_resolved} TLS=${rustproxy_tls_listen_resolved} ADMIN=${rustproxy_admin_listen_resolved}"
if [ -n "${rustproxy_malloc_conf}" ]; then
echo "RustProxy allocator: MALLOC_CONF=${rustproxy_malloc_conf}"
fi
echo "RustProxy exec: ${rustproxy_command} ${rustproxy_runtime_args}"
printf '%s\n' "${rustproxy_command} ${rustproxy_runtime_args}" > "${rustproxy_run_dir}/command.txt"
/bin/chmod 0640 "${rustproxy_run_dir}/command.txt"
}
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_poststart()
{
# daemon(8) may stay alive with -r even when rust-proxy exits immediately.
# Verify the supervised child so service/configd cannot report a false OK.
/bin/sleep 1
if _rp_pid="$(rustproxy_child_pid)"; then
echo "RustProxy child started as PID ${_rp_pid}"
return 0
fi
echo "RustProxy failed to stay running; stopping the restart supervisor." >&2
if [ -s "${rustproxy_log_file}" ]; then
echo "Last RustProxy log lines:" >&2
/usr/bin/tail -n 40 "${rustproxy_log_file}" >&2
fi
if [ -r "${pidfile}" ]; then
_rp_supervisor_pid="$(/bin/cat "${pidfile}" 2>/dev/null)"
case "${_rp_supervisor_pid}" in
''|*[!0-9]*) ;;
*) /bin/kill -TERM "${_rp_supervisor_pid}" 2>/dev/null || true ;;
esac
fi
/bin/sleep 1
/bin/rm -f "${child_pidfile}" "${pidfile}"
return 1
}
rustproxy_status()
{
if _rp_pid="$(rustproxy_child_pid)"; then
echo "rustproxy is running as pid ${_rp_pid}"
return 0
fi
_rp_supervisor_pid=""
if [ -r "${pidfile}" ]; then
_rp_supervisor_pid="$(/bin/cat "${pidfile}" 2>/dev/null)"
fi
case "${_rp_supervisor_pid}" in
''|*[!0-9]*) ;;
*)
if /bin/kill -0 "${_rp_supervisor_pid}" 2>/dev/null; then
echo "rustproxy is not running (daemon supervisor ${_rp_supervisor_pid} is waiting/restarting)"
return 1
fi
;;
esac
echo "rustproxy is not running"
return 1
}
rustproxy_poststop()
{
/bin/rm -f "${child_pidfile}" "${pidfile}"
}
run_rc_command "$1"
+4
View File
@@ -0,0 +1,4 @@
#!/bin/sh
/usr/local/sbin/configctl template reload OPNsense/RustProxy >/dev/null 2>&1 || true
/usr/local/sbin/configctl rustproxy boot >/dev/null 2>&1 || true
@@ -0,0 +1,20 @@
<?php
namespace OPNsense\RustProxy\Api;
use OPNsense\Base\ApiMutableServiceControllerBase;
class ServiceController extends ApiMutableServiceControllerBase
{
protected static $internalServiceClass = '\\OPNsense\\RustProxy\\RustProxy';
protected static $internalServiceTemplate = 'OPNsense/RustProxy';
protected static $internalServiceEnabled = 'general.enabled';
protected static $internalServiceName = 'rustproxy';
protected function reconfigureForceRestart()
{
// Listener addresses are process startup arguments, so Apply must
// always stop and start the service after regenerating the template.
return 1;
}
}
@@ -0,0 +1,11 @@
<?php
namespace OPNsense\RustProxy\Api;
use OPNsense\Base\ApiMutableModelControllerBase;
class SettingsController extends ApiMutableModelControllerBase
{
protected static $internalModelClass = 'OPNsense\RustProxy\RustProxy';
protected static $internalModelName = 'rustproxy';
}
@@ -0,0 +1,12 @@
<?php
namespace OPNsense\RustProxy;
class IndexController extends \OPNsense\Base\IndexController
{
public function indexAction()
{
$this->view->generalForm = $this->getForm('general');
$this->view->pick('OPNsense/RustProxy/index');
}
}
@@ -0,0 +1,79 @@
<form>
<field>
<label>General</label>
<type>header</type>
<static>true</static>
</field>
<field>
<id>rustproxy.general.enabled</id>
<label>Enabled</label>
<type>checkbox</type>
<help>Start RustProxy and keep it enabled after OPNsense restarts.</help>
</field>
<field>
<label>HTTP listener</label>
<type>header</type>
<static>true</static>
</field>
<field>
<id>rustproxy.general.proxyAddress</id>
<label>Bind address</label>
<type>text</type>
<help>HTTP bind address. Default: 0.0.0.0. Existing installations are migrated from the legacy combined listener automatically.</help>
</field>
<field>
<id>rustproxy.general.proxyPort</id>
<label>Port</label>
<type>text</type>
<help>HTTP port passed as --proxy-listen. Default: 8080.</help>
</field>
<field>
<label>HTTPS/TLS listener</label>
<type>header</type>
<static>true</static>
</field>
<field>
<id>rustproxy.general.tlsEnabled</id>
<label>Enable HTTPS/TLS listener</label>
<type>checkbox</type>
<help>When enabled, pass --tls-listen to RustProxy.</help>
</field>
<field>
<id>rustproxy.general.tlsAddress</id>
<label>Bind address</label>
<type>text</type>
<help>HTTPS/TLS bind address. Default: 0.0.0.0. Existing installations are migrated automatically.</help>
</field>
<field>
<id>rustproxy.general.tlsPort</id>
<label>Port</label>
<type>text</type>
<help>HTTPS/TLS port passed as --tls-listen. Default: 8181.</help>
</field>
<field>
<label>Administration</label>
<type>header</type>
<static>true</static>
</field>
<field>
<id>rustproxy.general.adminAddress</id>
<label>Bind address</label>
<type>text</type>
<help>Administration bind address. Default: 0.0.0.0. Existing installations are migrated automatically.</help>
</field>
<field>
<id>rustproxy.general.adminPort</id>
<label>Port</label>
<type>text</type>
<help>Administration port passed as --admin-listen. Default: 9090.</help>
</field>
<field>
<id>rustproxy.general.allowRemoteAdmin</id>
<label>Allow remote admin</label>
<type>checkbox</type>
<help>Pass --allow-remote-admin so the RustProxy panel accepts non-loopback clients. Keep firewall access restricted to trusted networks; do not expose the admin port to WAN unless this is intentional.</help>
</field>
</form>
@@ -0,0 +1,10 @@
<acl>
<page-services-rustproxy>
<name>Services: RustProxy</name>
<description>Allow access to RustProxy settings and service controls</description>
<patterns>
<pattern>ui/rustproxy/*</pattern>
<pattern>api/rustproxy/*</pattern>
</patterns>
</page-services-rustproxy>
</acl>
@@ -0,0 +1,7 @@
<menu>
<Services>
<RustProxy cssClass="fa fa-exchange fa-fw">
<General VisibleName="General" order="10" url="/ui/rustproxy/"/>
</RustProxy>
</Services>
</menu>
@@ -0,0 +1,68 @@
<?php
namespace OPNsense\RustProxy\Migrations;
use OPNsense\Base\BaseModelMigration;
/**
* Repair the 1.3.x split-listener transition.
*
* 1.2.x stored listeners as a single IP:port field. The first 1.3.x package
* introduced address/port fields without model defaults, so an upgrade could
* save fallback/new values while the still-valid legacy values remained in
* config.xml. Copy the legacy values once into the split fields so existing
* installations keep the exact ports they used before the upgrade.
*/
class M1_4_0 extends BaseModelMigration
{
private function splitListener($value)
{
$value = trim((string)$value);
if ($value === '') {
return null;
}
if (preg_match('/^\[([^\]]+)\]:(\d+)$/', $value, $matches)) {
$address = $matches[1];
$port = $matches[2];
} else {
$separator = strrpos($value, ':');
if ($separator === false) {
return null;
}
$address = substr($value, 0, $separator);
$port = substr($value, $separator + 1);
}
if ($address === '' || !ctype_digit($port)) {
return null;
}
$number = (int)$port;
if ($number < 1 || $number > 65535) {
return null;
}
return [$address, (string)$number];
}
private function migrateListener($general, $legacyField, $addressField, $portField)
{
$parts = $this->splitListener((string)$general->$legacyField);
if ($parts === null) {
return;
}
$general->$addressField = $parts[0];
$general->$portField = $parts[1];
}
public function run($model)
{
parent::run($model);
// Deliberately prefer valid 1.2.x values during this one migration.
// This also repairs hosts that already installed the broken 1.3.x model.
$this->migrateListener($model->general, 'proxyListen', 'proxyAddress', 'proxyPort');
$this->migrateListener($model->general, 'tlsListen', 'tlsAddress', 'tlsPort');
$this->migrateListener($model->general, 'adminListen', 'adminAddress', 'adminPort');
}
}
@@ -0,0 +1,9 @@
<?php
namespace OPNsense\RustProxy;
use OPNsense\Base\BaseModel;
class RustProxy extends BaseModel
{
}
@@ -0,0 +1,67 @@
<model>
<mount>//OPNsense/rustproxy</mount>
<version>1.4.0</version>
<description>RustProxy service settings</description>
<items>
<general>
<enabled type="BooleanField">
<Default>0</Default>
<Required>Y</Required>
</enabled>
<!--
Listener address/port are intentionally stored as separate fields.
OPNsense/FreeBSD installations in the field have produced malformed
combined IP:port values (for example 0.0.0.0:) when IPPortField is
round-tripped through config.xml/templates. Keep the old combined
fields below for upgrade compatibility, but new UI writes these.
-->
<proxyAddress type="NetworkField">
<Default>0.0.0.0</Default>
<Required>Y</Required>
<NetMaskAllowed>N</NetMaskAllowed>
<ValidationMessage>Enter a valid HTTP bind address, for example 0.0.0.0.</ValidationMessage>
</proxyAddress>
<proxyPort type="PortField">
<Default>8080</Default>
<Required>Y</Required>
<ValidationMessage>Enter a valid HTTP port between 1 and 65535.</ValidationMessage>
</proxyPort>
<tlsEnabled type="BooleanField">
<Default>1</Default>
<Required>Y</Required>
</tlsEnabled>
<tlsAddress type="NetworkField">
<Default>0.0.0.0</Default>
<Required>Y</Required>
<NetMaskAllowed>N</NetMaskAllowed>
<ValidationMessage>Enter a valid HTTPS/TLS bind address, for example 0.0.0.0.</ValidationMessage>
</tlsAddress>
<tlsPort type="PortField">
<Default>8181</Default>
<Required>Y</Required>
<ValidationMessage>Enter a valid HTTPS/TLS port between 1 and 65535.</ValidationMessage>
</tlsPort>
<adminAddress type="NetworkField">
<Default>0.0.0.0</Default>
<Required>Y</Required>
<NetMaskAllowed>N</NetMaskAllowed>
<ValidationMessage>Enter a valid administration bind address, for example 0.0.0.0.</ValidationMessage>
</adminAddress>
<adminPort type="PortField">
<Default>9090</Default>
<Required>Y</Required>
<ValidationMessage>Enter a valid administration port between 1 and 65535.</ValidationMessage>
</adminPort>
<allowRemoteAdmin type="BooleanField">
<Default>1</Default>
<Required>Y</Required>
</allowRemoteAdmin>
<!-- Legacy combined listener values retained for seamless upgrades. -->
<proxyListen type="IPPortField"/>
<tlsListen type="IPPortField"/>
<adminListen type="IPPortField"/>
</general>
</items>
</model>
@@ -0,0 +1,141 @@
<script type="text/javascript">
$(document).ready(function() {
function parseAdminListen(value) {
value = (value || '').trim();
if (value.length === 0) {
return null;
}
// Bracketed IPv6, for example [2001:db8::1]:9090.
if (value.charAt(0) === '[') {
const end = value.indexOf(']');
if (end === -1 || value.charAt(end + 1) !== ':') {
return null;
}
return {
host: value.substring(1, end),
port: value.substring(end + 2)
};
}
const separator = value.lastIndexOf(':');
if (separator <= 0) {
return null;
}
return {
host: value.substring(0, separator),
port: value.substring(separator + 1)
};
}
function formatUrlHost(host) {
return host.indexOf(':') !== -1 ? '[' + host + ']' : host;
}
function openRustProxyAdmin() {
$.getJSON('/api/rustproxy/settings/get')
.done(function(data) {
const general = data && data.rustproxy && data.rustproxy.general
? data.rustproxy.general
: {};
let listen = null;
const adminAddress = String(general.adminAddress || '').trim();
const adminPort = String(general.adminPort || '').trim();
if (adminAddress || adminPort) {
listen = {
host: adminAddress || '0.0.0.0',
port: adminPort || '9090'
};
} else {
// Upgrade compatibility with plugin model 1.2.x.
listen = parseAdminListen(general.adminListen || '0.0.0.0:9090');
}
const remoteValue = general.allowRemoteAdmin;
const allowRemote = remoteValue === true || remoteValue === 1 ||
['1', 'true', 'yes', 'on'].indexOf(String(remoteValue || '').toLowerCase()) !== -1;
if (!listen || !listen.port) {
BootstrapDialog.alert('{{ lang._("Invalid RustProxy admin listener configuration.") }}');
return;
}
if (!allowRemote) {
BootstrapDialog.alert('{{ lang._("Enable Allow remote admin and Apply the configuration before opening the RustProxy panel from another host.") }}');
return;
}
let targetHost = listen.host;
if (targetHost === '0.0.0.0' || targetHost === '::' || targetHost === '*') {
targetHost = window.location.hostname;
}
if (targetHost === '127.0.0.1' || targetHost === 'localhost' || targetHost === '::1') {
BootstrapDialog.alert('{{ lang._("The RustProxy admin listener is bound to loopback only. Set Admin listen to an address reachable from your network, for example 0.0.0.0:9090, then Apply.") }}');
return;
}
const url = 'http://' + formatUrlHost(targetHost) + ':' + listen.port + '/';
const adminWindow = window.open(url, '_blank');
if (adminWindow) {
adminWindow.opener = null;
}
})
.fail(function() {
BootstrapDialog.alert('{{ lang._("Unable to read RustProxy settings.") }}');
});
}
mapDataToFormUI({'frm_GeneralSettings': '/api/rustproxy/settings/get'}).done(function() {
updateServiceControlUI('rustproxy');
});
$('#reconfigureAct').SimpleActionButton({
onPreAction: function() {
const deferred = new $.Deferred();
saveFormToEndpoint(
'/api/rustproxy/settings/set',
'frm_GeneralSettings',
function() { deferred.resolve(); },
true,
function() { deferred.reject(); }
);
return deferred;
},
onAction: function() {
updateServiceControlUI('rustproxy');
}
});
$('#openAdminAct').click(function() {
openRustProxyAdmin();
});
});
</script>
<section class="page-content-main">
<div class="content-box">
{{ partial("layout_partials/base_form", ['fields': generalForm, 'id': 'frm_GeneralSettings']) }}
</div>
<br/>
<div class="content-box">
<div class="col-md-12">
<br/>
<button class="btn btn-primary"
id="reconfigureAct"
data-endpoint="/api/rustproxy/service/reconfigure"
data-label="{{ lang._('Apply') }}"
data-error-title="{{ lang._('Error reconfiguring RustProxy') }}"
type="button"></button>
<button class="btn btn-default"
id="openAdminAct"
type="button">
<span class="fa fa-external-link"></span>
{{ lang._('Open RustProxy Admin') }}
</button>
<br/><br/>
<p class="help-block">
{{ lang._('Remote administration is controlled by the RustProxy listener and OPNsense firewall rules. The plugin does not automatically create a WAN firewall rule for the admin port.') }}
</p>
</div>
</div>
</section>
+45
View File
@@ -0,0 +1,45 @@
#!/bin/sh
set -u
CONFIG_FILE="/usr/local/etc/rustproxy/opnsense.conf"
RC_SCRIPT="/usr/local/etc/rc.d/rustproxy"
enabled="0"
if [ -r "$CONFIG_FILE" ]; then
. "$CONFIG_FILE"
fi
start_service()
{
if [ "$enabled" != "1" ]; then
echo "RustProxy is disabled in OPNsense configuration"
return 0
fi
"$RC_SCRIPT" onestart
}
stop_service()
{
"$RC_SCRIPT" onestop >/dev/null 2>&1 || true
}
case "${1:-}" in
start|boot)
start_service
;;
stop)
stop_service
;;
restart|reload)
stop_service
start_service
;;
status)
"$RC_SCRIPT" status
;;
*)
echo "usage: $0 {start|stop|restart|reload|status|boot}" >&2
exit 64
;;
esac
@@ -0,0 +1,42 @@
[start]
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
@@ -0,0 +1 @@
rustproxy.conf:/usr/local/etc/rustproxy/opnsense.conf
@@ -0,0 +1,18 @@
enabled="{% if not helpers.empty('OPNsense.rustproxy.general.enabled') %}1{% else %}0{% endif %}"
# New split listener fields. Empty values are resolved by rc.d using the
# legacy combined field (upgrade path) and finally the built-in defaults.
rustproxy_proxy_address="{% if not helpers.empty('OPNsense.rustproxy.general.proxyAddress') %}{{ OPNsense.rustproxy.general.proxyAddress }}{% endif %}"
rustproxy_proxy_port="{% if not helpers.empty('OPNsense.rustproxy.general.proxyPort') %}{{ OPNsense.rustproxy.general.proxyPort }}{% endif %}"
rustproxy_tls_enabled="{% if not helpers.empty('OPNsense.rustproxy.general.tlsEnabled') %}1{% else %}0{% endif %}"
rustproxy_tls_address="{% if not helpers.empty('OPNsense.rustproxy.general.tlsAddress') %}{{ OPNsense.rustproxy.general.tlsAddress }}{% endif %}"
rustproxy_tls_port="{% if not helpers.empty('OPNsense.rustproxy.general.tlsPort') %}{{ OPNsense.rustproxy.general.tlsPort }}{% endif %}"
rustproxy_admin_address="{% if not helpers.empty('OPNsense.rustproxy.general.adminAddress') %}{{ OPNsense.rustproxy.general.adminAddress }}{% endif %}"
rustproxy_admin_port="{% if not helpers.empty('OPNsense.rustproxy.general.adminPort') %}{{ OPNsense.rustproxy.general.adminPort }}{% endif %}"
rustproxy_allow_remote_admin="{% if not helpers.empty('OPNsense.rustproxy.general.allowRemoteAdmin') %}1{% else %}0{% endif %}"
# Legacy 1.2.x combined settings. They are only used when the corresponding
# new split address/port fields are both empty.
rustproxy_proxy_listen_legacy="{% if not helpers.empty('OPNsense.rustproxy.general.proxyListen') %}{{ OPNsense.rustproxy.general.proxyListen }}{% endif %}"
rustproxy_tls_listen_legacy="{% if not helpers.empty('OPNsense.rustproxy.general.tlsListen') %}{{ OPNsense.rustproxy.general.tlsListen }}{% endif %}"
rustproxy_admin_listen_legacy="{% if not helpers.empty('OPNsense.rustproxy.general.adminListen') %}{{ OPNsense.rustproxy.general.adminListen }}{% endif %}"
+39
View File
@@ -0,0 +1,39 @@
#!/usr/bin/env bash
set -Eeuo pipefail
source /opt/rust-package-builder/scripts/lib.sh
SOURCE_DIR=${1:?usage: apply-freebsd-patches.sh /path/to/source}
PATCH_ROOT=/opt/rust-package-builder/opnsense/patches
[[ -f "$SOURCE_DIR/Cargo.toml" ]] || fatal "missing Cargo.toml in FreeBSD build source: $SOURCE_DIR"
VERSION=$(python3 - "$SOURCE_DIR/Cargo.toml" <<'PY'
import sys, tomllib
from pathlib import Path
with Path(sys.argv[1]).open('rb') as f:
print(tomllib.load(f)['package']['version'])
PY
)
PATCH_DIR="$PATCH_ROOT/$VERSION"
if [[ ! -d "$PATCH_DIR" ]]; then
log "no FreeBSD/OPNsense patch set for RustProxy $VERSION; using upstream source"
exit 0
fi
shopt -s nullglob
patches=("$PATCH_DIR"/*.patch)
(( ${#patches[@]} > 0 )) || fatal "FreeBSD patch directory is empty: $PATCH_DIR"
log "applying FreeBSD/OPNsense patches for RustProxy $VERSION"
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"
log "applied patch: $patch_name"
elif git -C "$SOURCE_DIR" apply --reverse --check "$patch_file" 2>/dev/null; then
log "patch already present: $patch_name"
else
fatal "patch does not apply cleanly: $patch_name (RustProxy $VERSION)"
fi
done
+51
View File
@@ -0,0 +1,51 @@
#!/usr/bin/env bash
set -Eeuo pipefail
source /opt/rust-package-builder/scripts/lib.sh
SOURCE_DIR=${1:?source path required}
OUTPUT_BINARY=${2:?output binary required}
BINARY_NAME=${3:?binary name required}
WORK_DIR=${4:?work directory required}
BUILD_SOURCE="$WORK_DIR/freebsd-source"
rm -rf "$BUILD_SOURCE"
mkdir -p "$BUILD_SOURCE" "$(dirname "$OUTPUT_BINARY")"
rsync -a --delete \
--exclude='.git/' \
--exclude='target/' \
"$SOURCE_DIR/" "$BUILD_SOURCE/"
if [[ "${APPLY_FREEBSD_PATCHES:-1}" == "1" ]]; then
/opt/rust-package-builder/scripts/apply-freebsd-patches.sh "$BUILD_SOURCE"
else
log "FreeBSD/OPNsense source patches disabled"
fi
FREEBSD_TARGET_VERSION=${FREEBSD_RELEASE%-RELEASE}
export CFLAGS_x86_64_unknown_freebsd="--target=x86_64-unknown-freebsd${FREEBSD_TARGET_VERSION} --sysroot=/opt/freebsd-sysroot"
export CXXFLAGS_x86_64_unknown_freebsd="--target=x86_64-unknown-freebsd${FREEBSD_TARGET_VERSION} --sysroot=/opt/freebsd-sysroot"
export CARGO_TARGET_X86_64_UNKNOWN_FREEBSD_RUSTFLAGS="-C link-arg=--target=x86_64-unknown-freebsd${FREEBSD_TARGET_VERSION} -C link-arg=--sysroot=/opt/freebsd-sysroot -C link-arg=-fuse-ld=lld"
log "cross-compiling $BINARY_NAME for x86_64-unknown-freebsd${FREEBSD_TARGET_VERSION}"
cd "$BUILD_SOURCE"
if [[ -f Cargo.lock ]]; then
cargo "+$FREEBSD_RUST_TOOLCHAIN" build --release --locked --target x86_64-unknown-freebsd
else
cargo "+$FREEBSD_RUST_TOOLCHAIN" build --release --target x86_64-unknown-freebsd
fi
BUILT="$CARGO_TARGET_DIR/x86_64-unknown-freebsd/release/$BINARY_NAME"
[[ -x "$BUILT" ]] || fatal "FreeBSD binary not found: $BUILT"
install -m 0555 "$BUILT" "$OUTPUT_BINARY"
desc=$(file -b "$OUTPUT_BINARY")
log "FreeBSD binary: $desc"
[[ "$desc" == *FreeBSD* ]] || fatal "cross-built ELF is not identified as FreeBSD"
header=$(llvm-readelf -h "$OUTPUT_BINARY")
printf '%s\n' "$header" | grep -qi FreeBSD || fatal "cross-built ELF header is not marked for FreeBSD"
dynamic=$(llvm-readelf -d "$OUTPUT_BINARY" || true)
if printf '%s\n' "$dynamic" | grep -q 'libc\.so\.6'; then
fatal "Linux libc detected in FreeBSD target binary"
fi
+92 -7
View File
@@ -11,8 +11,13 @@ PKG="$WORK/package"
OUT="$WORK/out"
REPOSITORY="$DATA_DIR/repository"
APTLY_CONF="$DATA_DIR/state/aptly.conf"
RELEASE_DIR=""
PUBLISHED=0
cleanup() {
if [[ "$PUBLISHED" != "1" && -n "$RELEASE_DIR" && -d "$RELEASE_DIR" ]]; then
rm -rf "$RELEASE_DIR"
fi
if [[ "${KEEP_WORKSPACE:-0}" != "1" ]]; then
rm -rf "$WORK"
else
@@ -28,6 +33,7 @@ git clone --quiet --no-tags --single-branch --branch "$SOURCE_BRANCH" "$SOURCE_R
git -C "$SRC" checkout --quiet --detach "$SHA"
ACTUAL_SHA=$(git -C "$SRC" rev-parse HEAD)
[[ "$ACTUAL_SHA" == "$SHA" ]] || fatal "checkout SHA mismatch"
SOURCE_REVISION=$(git -C "$SRC" rev-list --count "$SHA")
cd "$SRC"
META_JSON=$(cargo metadata --format-version 1 --no-deps)
@@ -109,13 +115,24 @@ fi
export PACKAGE_NAME VERSION BINARY_NAME DESCRIPTION LICENSE HOMEPAGE MAINTAINER PACKAGE_RELEASE
log "building version $VERSION ($PACKAGE_NAME / $BINARY_NAME)"
log "cargo build started"
if [[ "${RUN_LINUX_TESTS:-1}" == "1" ]]; then
log "cargo tests started"
if [[ -f Cargo.lock ]]; then
cargo test --release --locked
else
cargo test --release
fi
log "cargo tests completed"
fi
log "Linux cargo build started"
if [[ -f Cargo.lock ]]; then
cargo build --release --locked
else
cargo build --release
fi
log "cargo build completed"
log "Linux cargo build completed"
BINARY_PATH="$CARGO_TARGET_DIR/release/$BINARY_NAME"
[[ -x "$BINARY_PATH" ]] || fatal "built binary not found: $BINARY_PATH"
@@ -140,8 +157,24 @@ nfpm package --config "$PKG/nfpm-rpm.yaml" --packager rpm --target "$RPM_FILE"
ar t "$DEB_FILE" | grep '^_gpgorigin$' >/dev/null || fatal "DEB package does not contain nFPM GPG origin signature"
rpm -qpi "$RPM_FILE" | awk -F: '/^Signature/ && $0 !~ /\(none\)/ {ok=1} END{exit !ok}' || fatal "RPM package is not signed"
log "package signatures verified"
log "Linux package signatures verified"
# OPNsense is built from an isolated source copy. FreeBSD-specific patches never
# touch the Linux source checkout used above.
FREEBSD_BINARY="$OUT/rust-proxy-freebsd"
/opt/rust-package-builder/scripts/build-opnsense.sh "$SRC" "$FREEBSD_BINARY" "$BINARY_NAME" "$WORK"
if [[ "$SOURCE_REVISION" == "0" ]]; then
OPNSENSE_PACKAGE_VERSION="$VERSION"
else
OPNSENSE_PACKAGE_VERSION="${VERSION}_${SOURCE_REVISION}"
fi
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"
# Only after all package formats have been built successfully do we update the
# persistent Linux package stores and the internal APT repository.
cp -f "$DEB_FILE" "$DATA_DIR/packages/deb/"
cp -f "$RPM_FILE" "$DATA_DIR/packages/rpm/x86_64/"
@@ -178,7 +211,7 @@ gpg --batch --verify \
"$APT_PUBLIC/dists/$REPO_DISTRIBUTION/Release" >/dev/null 2>&1 || fatal "APT Release signature verification failed"
RELEASE_DIR="$REPOSITORY/releases/$BUILD_ID"
mkdir -p "$RELEASE_DIR/apt" "$RELEASE_DIR/rpm/x86_64"
mkdir -p "$RELEASE_DIR/apt" "$RELEASE_DIR/rpm/x86_64" "$RELEASE_DIR/opnsense"
CURRENT_TARGET=""
if [[ -L "$REPOSITORY/current" ]]; then
@@ -214,24 +247,76 @@ gpg --batch --verify \
"$RELEASE_DIR/rpm/x86_64/repodata/repomd.xml.asc" \
"$RELEASE_DIR/rpm/x86_64/repodata/repomd.xml" >/dev/null 2>&1 || fatal "RPM metadata signature verification failed"
log "staging OPNsense pkg repository"
if [[ -n "$CURRENT_TARGET" && -d "$CURRENT_TARGET/opnsense" ]]; then
rsync -a --delete --link-dest="$CURRENT_TARGET/opnsense"/ "$CURRENT_TARGET/opnsense"/ "$RELEASE_DIR/opnsense"/
fi
OPNSENSE_PACKAGE_DIR="$RELEASE_DIR/opnsense/$OPNSENSE_ABI/amd64"
mkdir -p "$OPNSENSE_PACKAGE_DIR"
# The previous OPNsense snapshot is hard-linked into this one. Unlink every
# file we are about to replace so the currently published snapshot stays immutable.
rm -f \
"$OPNSENSE_PACKAGE_DIR/$(basename "$OPNSENSE_PACKAGE_FILE")" \
"$RELEASE_DIR/opnsense/RustProxy.conf" \
"$RELEASE_DIR/opnsense/os-rustproxy-latest.pkg" \
"$RELEASE_DIR/opnsense/SHA256" \
"$RELEASE_DIR/opnsense/BUILD-INFO.txt" \
"$RELEASE_DIR/opnsense/index.html"
cp -f "$OPNSENSE_PACKAGE_FILE" "$OPNSENSE_PACKAGE_DIR/"
/opt/rust-package-builder/scripts/generate-opnsense-repo.py \
"$RELEASE_DIR/opnsense" --retention "$OPNSENSE_RETENTION"
cat > "$RELEASE_DIR/opnsense/RustProxy.conf" <<REPOCONF
RustProxy: {
url: "${OPNSENSE_REPOSITORY_URL%/}",
enabled: yes,
mirror_type: "none",
signature_type: "none"
}
REPOCONF
cp -f "$OPNSENSE_PACKAGE_FILE" "$RELEASE_DIR/opnsense/os-rustproxy-latest.pkg"
(cd "$RELEASE_DIR/opnsense" && sha256sum "$OPNSENSE_ABI/amd64/$(basename "$OPNSENSE_PACKAGE_FILE")") > "$RELEASE_DIR/opnsense/SHA256"
cat > "$RELEASE_DIR/opnsense/BUILD-INFO.txt" <<INFO
app_version=$VERSION
package_version=$OPNSENSE_PACKAGE_VERSION
source_url=$SOURCE_REPO
source_ref=$SOURCE_BRANCH
source_commit=$SHA
source_revision=$SOURCE_REVISION
opnsense_series=$OPNSENSE_ABI
freebsd_release=$FREEBSD_RELEASE
freebsd_abi=$FREEBSD_ABI
freebsd_arch=$FREEBSD_ARCH
package=$OPNSENSE_ABI/amd64/$(basename "$OPNSENSE_PACKAGE_FILE")
INFO
/opt/rust-package-builder/scripts/generate-opnsense-index.sh "$RELEASE_DIR/opnsense/index.html"
gpg --batch --armor --export "$GPG_KEY_ID" > "$RELEASE_DIR/repo-key.gpg"
[[ -s "$RELEASE_DIR/repo-key.gpg" ]] || fatal "public GPG key export failed"
/opt/rust-package-builder/scripts/generate-index.sh "$RELEASE_DIR/index.html" "$PACKAGE_NAME"
# Validate all externally visible endpoints before the atomic switch.
# Validate both public repositories before one atomic switch exposes either one.
[[ -s "$RELEASE_DIR/index.html" ]]
[[ -f "$RELEASE_DIR/apt/dists/$REPO_DISTRIBUTION/InRelease" ]]
[[ -f "$RELEASE_DIR/rpm/x86_64/repodata/repomd.xml" ]]
[[ -s "$RELEASE_DIR/opnsense/index.html" ]]
[[ -f "$RELEASE_DIR/opnsense/meta.conf" ]]
[[ -f "$RELEASE_DIR/opnsense/packagesite.pkg" ]]
[[ -f "$RELEASE_DIR/opnsense/data.pkg" ]]
[[ -f "$RELEASE_DIR/opnsense/os-rustproxy-latest.pkg" ]]
bsdtar -tf "$RELEASE_DIR/opnsense/packagesite.pkg" | grep -Fx 'packagesite.yaml' >/dev/null || fatal "invalid OPNsense packagesite.pkg"
bsdtar -xOf "$OPNSENSE_PACKAGE_DIR/$(basename "$OPNSENSE_PACKAGE_FILE")" +COMPACT_MANIFEST >/dev/null || fatal "invalid OPNsense package manifest"
log "publishing repository atomically"
log "publishing Linux and OPNsense repositories atomically"
rm -f "$REPOSITORY/current.new"
ln -s "releases/$BUILD_ID" "$REPOSITORY/current.new"
mv -Tf "$REPOSITORY/current.new" "$REPOSITORY/current"
PUBLISHED=1
printf '%s\n' "$SHA" | atomic_write "$DATA_DIR/state/last_commit"
printf '%s\n' "$VERSION" | atomic_write "$DATA_DIR/state/last_version"
printf '%s\n' "$PACKAGE_RELEASE" | atomic_write "$DATA_DIR/state/last_package_release"
printf '%s\n' "$OPNSENSE_PACKAGE_VERSION" | atomic_write "$DATA_DIR/state/last_opnsense_package_version"
printf '%s\n' "$BUILD_ID" | atomic_write "$DATA_DIR/state/last_release"
log "build completed: version=$VERSION package_release=$PACKAGE_RELEASE commit=$SHA release=$BUILD_ID"
log "build completed: version=$VERSION linux_release=$PACKAGE_RELEASE opnsense=$OPNSENSE_PACKAGE_VERSION commit=$SHA release=$BUILD_ID"
+24 -4
View File
@@ -5,18 +5,33 @@ source /opt/rust-package-builder/scripts/lib.sh
: "${SOURCE_REPO:?SOURCE_REPO is required}"
SOURCE_BRANCH=${SOURCE_BRANCH:-master}
: "${REPOSITORY_URL:?REPOSITORY_URL is required}"
: "${OPNSENSE_REPOSITORY_URL:?OPNSENSE_REPOSITORY_URL is required}"
POLL_INTERVAL=${POLL_INTERVAL:-300}
PACKAGE_RELEASE=${PACKAGE_RELEASE:-auto}
OPNSENSE_ABI=${OPNSENSE_ABI:-26.7}
OPNSENSE_RETENTION=${OPNSENSE_RETENTION:-10}
FREEBSD_RELEASE=${FREEBSD_RELEASE:-15.1-RELEASE}
FREEBSD_ABI=${FREEBSD_ABI:-FreeBSD:15:amd64}
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}
[[ "$POLL_INTERVAL" =~ ^[0-9]+$ ]] || fatal "POLL_INTERVAL must be an integer"
(( POLL_INTERVAL >= 1 )) || fatal "POLL_INTERVAL must be >= 1"
[[ "$PACKAGE_RELEASE" == "auto" || "$PACKAGE_RELEASE" =~ ^[1-9][0-9]*$ ]] || fatal "PACKAGE_RELEASE must be auto or a positive integer"
[[ "$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"
export DATA_DIR=${DATA_DIR:-/var/lib/package-builder}
export SOURCE_REPO SOURCE_BRANCH REPOSITORY_URL POLL_INTERVAL
export SOURCE_REPO SOURCE_BRANCH REPOSITORY_URL OPNSENSE_REPOSITORY_URL POLL_INTERVAL
export REPO_DISTRIBUTION=${REPO_DISTRIBUTION:-stable}
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 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}
@@ -24,10 +39,15 @@ export GNUPGHOME=${GNUPGHOME:-/run/package-builder/gnupg}
export CARGO_HOME="$DATA_DIR/cargo"
export CARGO_TARGET_DIR="$DATA_DIR/cargo/target"
for cmd in git cargo rustc nfpm aptly createrepo_c gpg rsync python3 python3.11 objdump ar rpm; do
for cmd in git cargo rustc rustup nfpm aptly createrepo_c gpg rsync python3 python3.11 \
objdump ar rpm clang llvm-readelf llvm-ar llvm-ranlib file tar zstd bsdtar sha256sum; do
require_cmd "$cmd"
done
rustc "+$FREEBSD_RUST_TOOLCHAIN" --version >/dev/null 2>&1 || fatal "FreeBSD Rust toolchain is unavailable: $FREEBSD_RUST_TOOLCHAIN"
rustup target list --toolchain "$FREEBSD_RUST_TOOLCHAIN" --installed | grep -Fx x86_64-unknown-freebsd >/dev/null \
|| fatal "Rust target x86_64-unknown-freebsd is not installed for $FREEBSD_RUST_TOOLCHAIN"
mkdir -p \
"$DATA_DIR/repository/releases" \
"$DATA_DIR/state" \
@@ -57,10 +77,10 @@ fi
/opt/rust-package-builder/scripts/init-storage.sh
log "builder started: branch=$SOURCE_BRANCH interval=${POLL_INTERVAL}s"
log "builder started: branch=$SOURCE_BRANCH interval=${POLL_INTERVAL}s opnsense=$OPNSENSE_ABI freebsd=$FREEBSD_RELEASE"
while true; do
if ! /opt/rust-package-builder/scripts/check-and-build.sh; then
log "ERROR: build cycle failed; published repository was not switched"
log "ERROR: build cycle failed; published repositories were not switched"
fi
sleep "$POLL_INTERVAL"
done
+16 -9
View File
@@ -6,7 +6,7 @@ OUT=${1:?output file required}
PACKAGE=${2:?package name required}
URL=$(trim_trailing_slash "$REPOSITORY_URL")
export OUT PACKAGE URL REPO_DISTRIBUTION REPO_COMPONENT
export OUT PACKAGE URL OPNSENSE_REPOSITORY_URL REPO_DISTRIBUTION REPO_COMPONENT
python3 <<'PY_END'
import html, os
out=os.environ['OUT']
@@ -14,14 +14,15 @@ package=os.environ['PACKAGE']
url=os.environ['URL']
dist=os.environ['REPO_DISTRIBUTION']
component=os.environ['REPO_COMPONENT']
opn_url=os.environ['OPNSENSE_REPOSITORY_URL'].rstrip('/')
h=html.escape
content=f'''<!doctype html>
<html lang="pl">
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="referrer" content="no-referrer">
<title>{h(package)} — repozytorium pakietów</title>
<title>{h(package)} — package repository</title>
<style>
:root{{color-scheme:light dark;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}}
body{{max-width:920px;margin:0 auto;padding:42px 20px 70px;line-height:1.55}}
@@ -34,9 +35,9 @@ a{{color:inherit}} .meta{{display:flex;gap:16px;flex-wrap:wrap;opacity:.82;font-
</style>
</head>
<body>
<h1>{h(package)} — repozytorium pakietów</h1>
<p class="lead">Podpisane pakiety DEB i RPM dla x86_64/amd64.</p>
<div class="meta"><span class="ok">Repozytorium aktywne</span><span>{h(url)}</span><span>APT + RPM/DNF</span></div>
<h1>{h(package)} — package repository</h1>
<p class="lead">Signed DEB and RPM packages for x86_64/amd64.</p>
<div class="meta"><span class="ok">Repository active</span><span>{h(url)}</span><span>APT + RPM/DNF</span></div>
<div class="card">
<h2>Debian / Ubuntu</h2>
@@ -68,13 +69,19 @@ sudo dnf install {h(package)}</code></pre>
</div>
<div class="card">
<h2>Pliki repozytorium</h2>
<h2>OPNsense / FreeBSD pkg</h2>
<p>The OPNsense plugin is published separately because it uses the FreeBSD pkg format.</p>
<p><a href="{h(opn_url)}/">{h(opn_url)}/</a></p>
</div>
<div class="card">
<h2>Repository files</h2>
<ul>
<li><a href="{h(url)}/repo-key.gpg">Publiczny klucz GPG</a></li>
<li><a href="{h(url)}/repo-key.gpg">Public GPG key</a></li>
<li><a href="{h(url)}/apt/dists/{h(dist)}/InRelease">APT InRelease</a></li>
<li><a href="{h(url)}/rpm/x86_64/repodata/repomd.xml">RPM repomd.xml</a></li>
</ul>
<p>Obsługiwane cele: Debian 12/13, Ubuntu 24.04/26.04, RHEL 9/10; architektura amd64/x86_64.</p>
<p>Supported targets: Debian 12/13, Ubuntu 24.04/26.04, RHEL 9/10; architecture: amd64/x86_64.</p>
</div>
</body>
</html>
+71
View File
@@ -0,0 +1,71 @@
#!/usr/bin/env bash
set -Eeuo pipefail
source /opt/rust-package-builder/scripts/lib.sh
OUT=${1:?output file required}
URL=$(trim_trailing_slash "$OPNSENSE_REPOSITORY_URL")
PACKAGE=${PACKAGE_NAME:-rustproxy}
export OUT URL PACKAGE OPNSENSE_ABI FREEBSD_ABI
python3 <<'PY'
import html
import os
out = os.environ["OUT"]
url = os.environ["URL"]
package = os.environ["PACKAGE"]
series = os.environ["OPNSENSE_ABI"]
abi = os.environ["FREEBSD_ABI"]
h = html.escape
content = f'''<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>RustProxy OPNsense repository</title>
<style>
:root{{color-scheme:light dark;font-family:system-ui,-apple-system,"Segoe UI",sans-serif}}
body{{max-width:900px;margin:0 auto;padding:42px 20px 70px;line-height:1.55}}
.card{{border:1px solid #8885;border-radius:12px;padding:18px;margin:18px 0}}
pre{{overflow:auto;padding:16px;border-radius:9px;background:#111;color:#eee}}
code{{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}}
</style>
</head>
<body>
<h1>RustProxy OPNsense repository</h1>
<p>FreeBSD pkg repository for the <code>os-rustproxy</code> OPNsense plugin.</p>
<div class="card">
<p><strong>Repository:</strong> {h(url)}</p>
<p><strong>OPNsense series:</strong> {h(series)} &nbsp; <strong>ABI:</strong> {h(abi)}</p>
</div>
<div class="card">
<h2>pkg repository configuration</h2>
<pre><code>mkdir -p /usr/local/etc/pkg/repos
cat &gt; /usr/local/etc/pkg/repos/RustProxy.conf &lt;&lt;'REPO'
RustProxy: {{
url: "{h(url)}",
enabled: yes,
mirror_type: "none",
signature_type: "none"
}}
REPO
pkg update -f
pkg install os-rustproxy</code></pre>
</div>
<div class="card">
<h2>Repository metadata</h2>
<ul>
<li><a href="{h(url)}/RustProxy.conf">RustProxy.conf</a> - ready pkg repo configuration</li>
<li><a href="{h(url)}/meta.conf">meta.conf</a></li>
<li><a href="{h(url)}/packagesite.pkg">packagesite.pkg</a></li>
<li><a href="{h(url)}/data.pkg">data.pkg</a></li>
<li><a href="{h(url)}/os-rustproxy-latest.pkg">latest package</a></li>
</ul>
</div>
</body>
</html>
'''
with open(out, "w", encoding="utf-8") as f:
f.write(content)
PY
+141
View File
@@ -0,0 +1,141 @@
#!/usr/bin/env python3
import argparse
import hashlib
import io
import json
import os
import subprocess
import tarfile
import time
from collections import defaultdict
from pathlib import Path
CATALOG_FILES = {"data.pkg", "packagesite.pkg"}
LATEST_NAMES = {"latest.pkg", "os-rustproxy-latest.pkg"}
def sha256(path: Path) -> str:
h = hashlib.sha256()
with path.open("rb") as f:
for chunk in iter(lambda: f.read(1024 * 1024), b""):
h.update(chunk)
return h.hexdigest()
def read_manifest(pkg: Path) -> dict:
for member in ("+COMPACT_MANIFEST", "+MANIFEST"):
p = subprocess.run(
["bsdtar", "-xOf", str(pkg), member],
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
check=False,
)
if p.returncode == 0 and p.stdout:
return json.loads(p.stdout.decode("utf-8"))
raise RuntimeError(f"{pkg.name}: missing +COMPACT_MANIFEST / +MANIFEST")
def is_payload_pkg(path: Path) -> bool:
return (
path.is_file()
and path.suffix == ".pkg"
and path.name not in CATALOG_FILES
and path.name not in LATEST_NAMES
and not path.name.endswith(".tmp.pkg")
)
def collect_packages(repo: Path):
packages = []
for path in sorted(repo.rglob("*.pkg")):
if not is_payload_pkg(path):
continue
manifest = read_manifest(path)
manifest["path"] = path.relative_to(repo).as_posix()
manifest["pkgsize"] = path.stat().st_size
manifest["sum"] = sha256(path)
packages.append((path, manifest))
return packages
def enforce_retention(packages, retention: int) -> bool:
if retention <= 0:
return False
groups = defaultdict(list)
for path, manifest in packages:
# Keep the requested number per repository subdirectory/ABI and package name.
key = (path.parent, manifest.get("name", path.stem))
groups[key].append((path, manifest))
removed = False
for (_parent, _name), entries in groups.items():
entries.sort(key=lambda item: item[0].stat().st_mtime_ns, reverse=True)
for path, _manifest in entries[retention:]:
try:
path.unlink()
removed = True
print(f"[opnsense-repo] retention: removed {path}", flush=True)
except FileNotFoundError:
pass
return removed
def make_tar(dst: Path, member_name: str, payload: bytes):
tmp = dst.with_name(dst.name + ".tmp")
with tarfile.open(tmp, "w", format=tarfile.PAX_FORMAT) as tf:
info = tarfile.TarInfo(member_name)
info.size = len(payload)
info.mode = 0o644
info.mtime = int(time.time())
tf.addfile(info, io.BytesIO(payload))
os.replace(tmp, dst)
def rebuild(repo: Path, retention: int):
packages = collect_packages(repo)
if enforce_retention(packages, retention):
packages = collect_packages(repo)
manifests = [manifest for _, manifest in packages]
packagesite = b"".join(
json.dumps(m, separators=(",", ":"), ensure_ascii=False).encode("utf-8") + b"\n"
for m in manifests
)
data = json.dumps(
{"groups": [], "expired_packages": [], "packages": manifests},
separators=(",", ":"),
ensure_ascii=False,
).encode("utf-8")
make_tar(repo / "packagesite.pkg", "packagesite.yaml", packagesite)
make_tar(repo / "data.pkg", "data", data)
meta = (
'version: 1;\n'
'packing_format: "tar";\n'
'manifests: "packagesite.yaml";\n'
'manifests_archive: "packagesite";\n'
'data: "data";\n'
'data_archive: "data";\n'
'filesite: "files";\n'
'filesite_archive: "files";\n'
)
tmp = repo / "meta.conf.tmp"
tmp.write_text(meta, encoding="utf-8")
os.replace(tmp, repo / "meta.conf")
print(f"[opnsense-repo] rebuilt: {len(manifests)} package(s)", flush=True)
def main():
parser = argparse.ArgumentParser(description="Build a static FreeBSD pkg repository catalogue")
parser.add_argument("repo", type=Path, help="repository root")
parser.add_argument("--retention", type=int, default=10, help="versions kept per package/ABI; 0 disables pruning")
args = parser.parse_args()
repo = args.repo.resolve()
repo.mkdir(parents=True, exist_ok=True)
rebuild(repo, args.retention)
if __name__ == "__main__":
main()
+2 -1
View File
@@ -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 repo-key.gpg index.html; do
for item in apt rpm opnsense repo-key.gpg index.html; do
if [[ -e "$REPOSITORY/$item" && ! -L "$REPOSITORY/$item" ]]; then
fatal "$REPOSITORY/$item exists and is not a symlink; refusing to overwrite"
fi
@@ -42,5 +42,6 @@ done
ln -sfn current/apt "$REPOSITORY/apt"
ln -sfn current/rpm "$REPOSITORY/rpm"
ln -sfn current/opnsense "$REPOSITORY/opnsense"
ln -sfn current/repo-key.gpg "$REPOSITORY/repo-key.gpg"
ln -sfn current/index.html "$REPOSITORY/index.html"
+143
View File
@@ -0,0 +1,143 @@
#!/usr/bin/env bash
set -Eeuo pipefail
source /opt/rust-package-builder/scripts/lib.sh
SOURCE_DIR=${1:?source path required}
BINARY=${2:?FreeBSD binary required}
PACKAGE_FILE=${3:?output package file required}
PACKAGE_VERSION=${4:?OPNsense package version required}
PLUGIN_ROOT=/opt/rust-package-builder/opnsense/plugin
FREEBSD_ABI=${FREEBSD_ABI:-FreeBSD:15:amd64}
FREEBSD_ARCH=${FREEBSD_ARCH:-freebsd:15:x86:64}
for tool in python3 sha256sum tar zstd file; do
require_cmd "$tool"
done
[[ -f "$SOURCE_DIR/Cargo.toml" ]] || fatal "missing Cargo.toml in $SOURCE_DIR"
[[ -f "$BINARY" ]] || fatal "missing cross-built binary: $BINARY"
[[ -d "$PLUGIN_ROOT/src" ]] || fatal "missing embedded OPNsense plugin source: $PLUGIN_ROOT/src"
binary_desc=$(file -b "$BINARY")
[[ "$binary_desc" == *FreeBSD* ]] || fatal "refusing to package a non-FreeBSD binary: $binary_desc"
WORK=$(mktemp -d "${PACKAGE_FILE}.work.XXXXXX")
trap 'rm -rf "$WORK"' EXIT
STAGE="$WORK/stage"
META="$WORK/meta"
ARCHIVE="$WORK/os-rustproxy.tar"
PAYLOAD_LIST="$WORK/payload.list"
mkdir -p "$STAGE/usr/local" "$META" "$(dirname "$PACKAGE_FILE")"
cp -a "$PLUGIN_ROOT/src/." "$STAGE/usr/local/"
mkdir -p "$STAGE/usr/local/sbin"
install -m 0555 "$BINARY" "$STAGE/usr/local/sbin/rust-proxy"
chmod 0555 "$STAGE/usr/local/etc/rc.d/rustproxy"
chmod 0555 "$STAGE/usr/local/etc/rc.syshook.d/10-rustproxy.start"
chmod 0555 "$STAGE/usr/local/opnsense/scripts/rustproxy/service.sh"
POST_INSTALL_FILE="$PLUGIN_ROOT/+POST_INSTALL.pre"
PRE_DEINSTALL_FILE="$PLUGIN_ROOT/+PRE_DEINSTALL.pre"
python3 - "$STAGE" "$META" "$PACKAGE_VERSION" "$FREEBSD_ABI" "$FREEBSD_ARCH" \
"$POST_INSTALL_FILE" "$PRE_DEINSTALL_FILE" <<'PY'
import hashlib
import json
from pathlib import Path
import sys
stage = Path(sys.argv[1])
meta_dir = Path(sys.argv[2])
version = sys.argv[3]
abi = sys.argv[4]
arch = sys.argv[5]
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 p in sorted(stage.rglob("*")):
if p.is_symlink():
files["/" + p.relative_to(stage).as_posix()] = "-"
elif p.is_file():
files["/" + p.relative_to(stage).as_posix()] = hashlib.sha256(p.read_bytes()).hexdigest()
flatsize += p.stat().st_size
common = {
"name": "os-rustproxy",
"version": version,
"origin": "opnsense/os-rustproxy",
"comment": "RustProxy reverse proxy service",
"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",
"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_dir / "+COMPACT_MANIFEST").write_text(
json.dumps(common, separators=(",", ":"), ensure_ascii=False) + "\n",
encoding="utf-8",
)
(meta_dir / "+MANIFEST").write_text(
json.dumps(manifest, separators=(",", ":"), ensure_ascii=False) + "\n",
encoding="utf-8",
)
PY
(
cd "$STAGE"
find . \( -type f -o -type l \) -print | LC_ALL=C sort | sed 's#^\./##' > "$PAYLOAD_LIST"
)
[[ -s "$PAYLOAD_LIST" ]] || fatal "OPNsense 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 "$PAYLOAD_LIST"
python3 - "$ARCHIVE" "$META/+MANIFEST" <<'PY'
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:]
if any(m.isdir() for m in payload):
raise SystemExit("directory entries are not allowed in OPNsense package payload")
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(f"package payload does not match +MANIFEST; missing={missing[:5]} extra={extra[:5]}")
PY
zstd -q -f -19 -T0 "$ARCHIVE" -o "$PACKAGE_FILE"
zstd -dc "$PACKAGE_FILE" | tar -tf - >/dev/null
log "created OPNsense package: $PACKAGE_FILE"
+69 -98
View File
@@ -10,40 +10,24 @@ DATA_DIR="$ROOT/data"
SERVICE="rust-package-builder"
HTTP_SERVICE="repository-http"
DEFAULT_HTTP_PORT=18765
DEFAULT_OPNSENSE_HTTP_PORT=18766
log() {
printf '[start] %s\n' "$*"
}
warn() {
printf '[start] WARN: %s\n' "$*" >&2
}
die() {
printf '[start] ERROR: %s\n' "$*" >&2
exit 1
}
require_cmd() {
command -v "$1" >/dev/null 2>&1 || die "brak wymaganego polecenia: $1"
}
log() { printf '[start] %s\n' "$*"; }
warn() { printf '[start] WARN: %s\n' "$*" >&2; }
die() { printf '[start] ERROR: %s\n' "$*" >&2; exit 1; }
require_cmd() { command -v "$1" >/dev/null 2>&1 || die "brak wymaganego polecenia: $1"; }
write_env_file() {
local source_repo=${SOURCE_REPO:-}
local repository_url=${REPOSITORY_URL:-}
cat > "$ENV_FILE" <<ENV
SOURCE_REPO=$source_repo
REPOSITORY_URL=$repository_url
SOURCE_REPO=${SOURCE_REPO:-}
REPOSITORY_URL=${REPOSITORY_URL:-}
OPNSENSE_REPOSITORY_URL=${OPNSENSE_REPOSITORY_URL:-}
ENV
chmod 0600 "$ENV_FILE" 2>/dev/null || true
}
update_env_var() {
local key=$1
local value=$2
local tmp="$ENV_FILE.tmp.$$"
local key=$1 value=$2 tmp="$ENV_FILE.tmp.$$"
awk -v key="$key" -v value="$value" '
BEGIN { found=0 }
index($0, key "=") == 1 { print key "=" value; found=1; next }
@@ -55,58 +39,53 @@ update_env_var() {
}
load_env_file() {
# .env is intentionally limited to simple KEY=value entries used by Compose.
# shellcheck disable=SC1090
set -a
# shellcheck disable=SC1090
source "$ENV_FILE"
set +a
}
normalize_http_settings() {
local url=${REPOSITORY_URL%/}
local rest hostpart explicit_port=""
normalize_repo_url() {
local url_var=$1 port_var=$2 default_port=$3 label=$4
local url=${!url_var:-} port=${!port_var:-} rest hostpart explicit_port=""
HTTP_PORT=${HTTP_PORT:-}
url=${url%/}
[[ -n "$url" ]] || die "$url_var jest wymagane"
case "$url" in
http://*)
rest=${url#http://}
[[ "$rest" != */* ]] || die "REPOSITORY_URL dla wbudowanego HTTP nie moze zawierac sciezki: $REPOSITORY_URL"
hostpart=$rest
if [[ "$hostpart" =~ ^\[[^]]+\]:([0-9]+)$ ]]; then
explicit_port=${BASH_REMATCH[1]}
elif [[ "$hostpart" =~ ^[^:]+:([0-9]+)$ ]]; then
explicit_port=${BASH_REMATCH[1]}
fi
if [[ -n "$explicit_port" ]]; then
HTTP_PORT=$explicit_port
else
HTTP_PORT=${HTTP_PORT:-$DEFAULT_HTTP_PORT}
url="${url}:${HTTP_PORT}"
REPOSITORY_URL=$url
export REPOSITORY_URL
update_env_var REPOSITORY_URL "$REPOSITORY_URL"
log "REPOSITORY_URL bez portu - ustawiono $REPOSITORY_URL"
fi
;;
https://*)
# HTTPS is supported as a public URL only when an external TLS proxy points
# to the local micro HTTP service. The embedded service itself is HTTP only.
HTTP_PORT=${HTTP_PORT:-$DEFAULT_HTTP_PORT}
warn "REPOSITORY_URL uzywa HTTPS. Wbudowany serwer nasluchuje po zwyklym HTTP na porcie $HTTP_PORT; HTTPS wymaga zewnetrznego reverse proxy/TLS."
;;
*)
die "REPOSITORY_URL musi zaczynac sie od http:// albo https://"
;;
http://*) rest=${url#http://} ;;
https://*) rest=${url#https://} ;;
*) die "$url_var musi zaczynac sie od http:// albo https://" ;;
esac
[[ "$HTTP_PORT" =~ ^[0-9]+$ ]] || die "HTTP_PORT musi byc liczba"
(( HTTP_PORT >= 1024 && HTTP_PORT <= 65535 )) || die "HTTP_PORT musi byc z zakresu 1024-65535"
[[ "$rest" != */* ]] || die "$url_var nie moze zawierac sciezki: $url"
hostpart=$rest
if [[ "$hostpart" =~ ^\[[^]]+\]:([0-9]+)$ ]]; then
explicit_port=${BASH_REMATCH[1]}
elif [[ "$hostpart" =~ ^[^:]+:([0-9]+)$ ]]; then
explicit_port=${BASH_REMATCH[1]}
fi
export HTTP_PORT
update_env_var HTTP_PORT "$HTTP_PORT"
if [[ "$url" == http://* && -n "$explicit_port" ]]; then
port=$explicit_port
else
port=${port:-$default_port}
if [[ "$url" == http://* && -z "$explicit_port" ]]; then
url="${url}:${port}"
log "$label URL bez portu - ustawiono $url"
elif [[ "$url" == https://* ]]; then
warn "$label uzywa HTTPS. Wbudowany nginx nasluchuje lokalnie po HTTP na porcie $port; TLS wymaga reverse proxy."
fi
fi
[[ "$port" =~ ^[0-9]+$ ]] || die "$port_var musi byc liczba"
(( port >= 1024 && port <= 65535 )) || die "$port_var musi byc z zakresu 1024-65535"
printf -v "$url_var" '%s' "$url"
printf -v "$port_var" '%s' "$port"
export "$url_var" "$port_var"
update_env_var "$url_var" "$url"
update_env_var "$port_var" "$port"
}
require_cmd docker
@@ -123,40 +102,40 @@ else
die "brak Docker Compose (docker compose lub docker-compose)"
fi
if [[ ! -f "$ENV_FILE" ]]; then
write_env_file
fi
[[ -f "$ENV_FILE" ]] || write_env_file
load_env_file
: "${SOURCE_REPO:?Uzupelnij SOURCE_REPO w .env albo ustaw zmienna przed uruchomieniem start.sh}"
: "${REPOSITORY_URL:?Uzupelnij REPOSITORY_URL w .env albo ustaw zmienna przed uruchomieniem start.sh}"
: "${REPOSITORY_URL:?Uzupelnij REPOSITORY_URL dla APT/RPM}"
: "${OPNSENSE_REPOSITORY_URL:?Uzupelnij OPNSENSE_REPOSITORY_URL dla repo FreeBSD pkg}"
case "$SOURCE_REPO" in
http://*|https://*|ssh://*|git@*|file://*) ;;
*) die "SOURCE_REPO ma nieobslugiwany format: $SOURCE_REPO" ;;
esac
normalize_http_settings
normalize_repo_url REPOSITORY_URL HTTP_PORT "$DEFAULT_HTTP_PORT" "APT/RPM"
normalize_repo_url OPNSENSE_REPOSITORY_URL OPNSENSE_HTTP_PORT "$DEFAULT_OPNSENSE_HTTP_PORT" "OPNsense"
[[ "$HTTP_PORT" != "$OPNSENSE_HTTP_PORT" ]] || die "HTTP_PORT i OPNSENSE_HTTP_PORT musza byc rozne"
mkdir -p "$DATA_DIR/repository" "$DATA_DIR/state" "$DATA_DIR/cargo" "$DATA_DIR/work" "$GPG_DIR"
chmod 0700 "$GPG_DIR" 2>/dev/null || true
log "budowanie obrazu buildera"
log "budowanie obrazu buildera (Linux + FreeBSD cross-toolchain)"
"${COMPOSE[@]}" --env-file "$ENV_FILE" build "$SERVICE"
if [[ ! -s "$GPG_DIR/private-key.asc" ]]; then
log "brak klucza GPG - generowanie stalego klucza podpisujacego"
log "brak klucza GPG - generowanie stalego klucza podpisujacego Linux APT/RPM"
IMAGE_REF=$("${COMPOSE[@]}" --env-file "$ENV_FILE" config | awk '''
IMAGE_REF=$("${COMPOSE[@]}" --env-file "$ENV_FILE" config | awk '
$1 == "rust-package-builder:" { in_builder=1; next }
in_builder && $1 == "image:" { print $2; exit }
''')
')
[[ -n "$IMAGE_REF" ]] || die "nie mozna ustalic nazwy zbudowanego obrazu buildera"
docker image inspect "$IMAGE_REF" >/dev/null 2>&1 || die "zbudowany obraz nie istnieje: $IMAGE_REF"
uid=$(id -u)
gid=$(id -g)
docker run --rm \
--user "$uid:$gid" \
--entrypoint /bin/bash \
@@ -168,7 +147,6 @@ if [[ ! -s "$GPG_DIR/private-key.asc" ]]; then
export GNUPGHOME=/tmp/rpb-keygen
mkdir -p "$GNUPGHOME"
chmod 0700 "$GNUPGHOME"
cat > /tmp/gpg-batch <<"KEY"
Key-Type: RSA
Key-Length: 3072
@@ -179,11 +157,9 @@ Expire-Date: 0
%no-protection
%commit
KEY
gpg --batch --generate-key /tmp/gpg-batch
fingerprint=$(gpg --batch --with-colons --list-secret-keys | awk -F: '\''/^sec:/ {seen=1; next} seen && /^fpr:/ {print $10; exit}'\'')
test -n "$fingerprint"
gpg --batch --armor --export-secret-keys "$fingerprint" > /out/private-key.asc
gpg --batch --armor --export "$fingerprint" > /out/public-key.asc
printf "%s\n" "$fingerprint" > /out/fingerprint
@@ -197,30 +173,25 @@ else
log "uzywanie istniejacego klucza GPG"
fi
log "uruchamianie buildera i mikro HTTP przez Docker Compose"
log "uruchamianie buildera i wspolnego nginx przez Docker Compose"
"${COMPOSE[@]}" --env-file "$ENV_FILE" up -d "$SERVICE" "$HTTP_SERVICE"
fingerprint=""
if [[ -r "$GPG_DIR/fingerprint" ]]; then
fingerprint=$(tr -d '[:space:]' < "$GPG_DIR/fingerprint")
fi
[[ -r "$GPG_DIR/fingerprint" ]] && fingerprint=$(tr -d '[:space:]' < "$GPG_DIR/fingerprint")
log "gotowe"
printf '\n'
printf 'SOURCE_REPO: %s\n' "$SOURCE_REPO"
printf 'REPOSITORY_URL: %s\n' "$REPOSITORY_URL"
printf 'HTTP port: %s\n' "$HTTP_PORT"
[[ -n "$fingerprint" ]] && printf 'GPG fingerprint: %s\n' "$fingerprint"
printf 'Publiczny katalog: %s\n' "$DATA_DIR/repository"
printf '\n'
printf 'Repo/strona: %s/\n' "${REPOSITORY_URL%/}"
printf 'Healthcheck: http://127.0.0.1:%s/healthz\n' "$HTTP_PORT"
printf '\n'
printf 'Podglad logow buildera:\n '
printf '\nSOURCE_REPO: %s\n' "$SOURCE_REPO"
printf 'APT/RPM URL: %s/\n' "${REPOSITORY_URL%/}"
printf 'APT/RPM local HTTP port: %s\n' "$HTTP_PORT"
printf 'OPNsense pkg URL: %s/\n' "${OPNSENSE_REPOSITORY_URL%/}"
printf 'OPNsense local HTTP port: %s\n' "$OPNSENSE_HTTP_PORT"
[[ -n "$fingerprint" ]] && printf 'GPG fingerprint: %s\n' "$fingerprint"
printf 'Publiczny katalog: %s\n\n' "$DATA_DIR/repository"
printf 'Health Linux: http://127.0.0.1:%s/healthz\n' "$HTTP_PORT"
printf 'Health OPNsense: http://127.0.0.1:%s/healthz\n\n' "$OPNSENSE_HTTP_PORT"
printf 'Logi buildera:\n '
printf '%q ' "${COMPOSE[@]}"
printf 'logs -f %q\n' "$SERVICE"
printf 'Podglad logow HTTP:\n '
printf 'Logi HTTP:\n '
printf '%q ' "${COMPOSE[@]}"
printf 'logs -f %q\n' "$HTTP_SERVICE"
printf '\n'
printf 'Builder natychmiast sprawdzi branch master (lub SOURCE_BRANCH, jezeli ustawiono go opcjonalnie) i po udanym buildzie opublikuje repozytorium.\n'
+31 -18
View File
@@ -5,36 +5,49 @@ ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
for f in "$ROOT"/start.sh "$ROOT"/scripts/*.sh "$ROOT"/tests/*.sh; do
bash -n "$f"
done
python3 -m py_compile "$ROOT/scripts/generate-opnsense-repo.py"
# Main Linux builder.
grep -q 'FROM rockylinux:9' "$ROOT/Dockerfile"
grep -q 'dnf -y install --allowerasing' "$ROOT/Dockerfile"
grep -q 'python3.11' "$ROOT/Dockerfile"
grep -q 'RUST_PROXY_PYTHON=/usr/bin/python3.11' "$ROOT/Dockerfile"
grep -q 'nfpm package' "$ROOT/scripts/build-release.sh"
grep -q 'aptly .*publish' "$ROOT/scripts/build-release.sh"
grep -q 'createrepo_c' "$ROOT/scripts/build-release.sh"
grep -q 'current.new' "$ROOT/scripts/build-release.sh"
grep -q 'last_package_release' "$ROOT/scripts/build-release.sh"
grep -q 'repo add -force-replace' "$ROOT/scripts/build-release.sh"
grep -q '/run/package-builder/gnupg' "$ROOT/scripts/entrypoint.sh"
grep -q 'rustproxy-overview.json' "$ROOT/scripts/prepare-package.sh"
grep -q 'debian/${PACKAGE_NAME}.service' "$ROOT/scripts/prepare-package.sh"
grep -q 'private-key.asc' "$ROOT/start.sh"
grep -q 'docker compose' "$ROOT/start.sh"
grep -q 'Key-Type: RSA' "$ROOT/start.sh"
grep -q 'DEFAULT_HTTP_PORT=18765' "$ROOT/start.sh"
grep -q 'repository-http' "$ROOT/start.sh"
grep -Fq '${SOURCE_REPO:?SOURCE_REPO is required}' "$ROOT/docker-compose.yml"
# Integrated FreeBSD/OPNsense build.
grep -q 'FREEBSD_RELEASE=15.1-RELEASE' "$ROOT/Dockerfile"
grep -q 'x86_64-unknown-freebsd' "$ROOT/Dockerfile"
grep -q '/opt/freebsd-sysroot' "$ROOT/Dockerfile"
grep -q 'build-opnsense.sh' "$ROOT/scripts/build-release.sh"
grep -q 'package-opnsense.sh' "$ROOT/scripts/build-release.sh"
grep -q 'generate-opnsense-repo.py' "$ROOT/scripts/build-release.sh"
grep -q 'OPNSENSE_REPOSITORY_URL' "$ROOT/scripts/entrypoint.sh"
grep -q 'OPNSENSE_RETENTION' "$ROOT/scripts/entrypoint.sh"
test -f "$ROOT/opnsense/plugin/src/etc/rc.d/rustproxy"
test -f "$ROOT/opnsense/patches/0.20.0/0001-opnsense-build.patch"
# One atomic switch exposes Linux and OPNsense together.
grep -q 'current.new' "$ROOT/scripts/build-release.sh"
grep -q 'current/opnsense' "$ROOT/scripts/init-storage.sh"
grep -q 'last_opnsense_package_version' "$ROOT/scripts/build-release.sh"
# Two public endpoints from one nginx container.
grep -Fq '${REPOSITORY_URL:?REPOSITORY_URL is required}' "$ROOT/docker-compose.yml"
grep -q 'nginxinc/nginx-unprivileged:stable-alpine-slim' "$ROOT/docker-compose.yml"
grep -q 'read_only: true' "$ROOT/docker-compose.yml"
grep -q 'no-new-privileges:true' "$ROOT/docker-compose.yml"
grep -q 'cap_drop:' "$ROOT/docker-compose.yml"
grep -Fq '${OPNSENSE_REPOSITORY_URL:?OPNSENSE_REPOSITORY_URL is required}' "$ROOT/docker-compose.yml"
grep -Fq '0.0.0.0:${HTTP_PORT:-18765}:8080' "$ROOT/docker-compose.yml"
grep -q 'server_tokens off' "$ROOT/http/nginx.conf"
grep -Fq '0.0.0.0:${OPNSENSE_HTTP_PORT:-18766}:8081' "$ROOT/docker-compose.yml"
grep -q 'listen 8080' "$ROOT/http/nginx.conf"
grep -q 'listen 8081' "$ROOT/http/nginx.conf"
grep -q 'root /srv/repository/opnsense' "$ROOT/http/nginx.conf"
grep -q 'GET|HEAD' "$ROOT/http/nginx.conf"
grep -q 'autoindex off' "$ROOT/http/nginx.conf"
grep -q 'X-Content-Type-Options' "$ROOT/http/nginx.conf"
grep -q 'healthz' "$ROOT/http/nginx.conf"
# start.sh and example configuration expose both URLs/ports.
grep -q 'DEFAULT_HTTP_PORT=18765' "$ROOT/start.sh"
grep -q 'DEFAULT_OPNSENSE_HTTP_PORT=18766' "$ROOT/start.sh"
grep -q 'OPNSENSE_REPOSITORY_URL=https://opn-rustproxy.linuxiarz.pl' "$ROOT/.env.example"
echo 'static checks: OK'