From efa486ba4f28ca350f0a236dfcbffc250e70d5d8 Mon Sep 17 00:00:00 2001 From: vicrodh <103399283+vicrodh@users.noreply.github.com> Date: Sat, 25 Jul 2026 12:05:56 -0600 Subject: [PATCH] feat(packaging+log): standalone qbzd packages, IP masking in logs - nfpm-qbzd.yaml (deb/rpm, libasound-only dep) + release workflows build the standalone qbzd packages per arch; dual dispatch to the future qbz-rpm repo (continue-on-error until token scope lands). - Gentoo overlay templates: qbzd (source build) + qbzd-bin (tarball repack) with systemd user unit + completions. - Log redaction layer 3: IPv4 -> *.*.*., IPv6 -> *:*:, with version/timestamp false-positive guards (qbz-log 10/10 tests). km --- .github/workflows/release-linux-aarch64.yml | 18 +++++ .github/workflows/release-linux.yml | 19 +++++ crates/qbz-log/src/redact.rs | 78 ++++++++++++++++++++ packaging/gentoo/qbzd-bin.ebuild.template | 73 +++++++++++++++++++ packaging/gentoo/qbzd.ebuild.template | 79 +++++++++++++++++++++ packaging/nfpm/nfpm-qbzd.yaml | 62 ++++++++++++++++ 6 files changed, 329 insertions(+) create mode 100644 packaging/gentoo/qbzd-bin.ebuild.template create mode 100644 packaging/gentoo/qbzd.ebuild.template create mode 100644 packaging/nfpm/nfpm-qbzd.yaml diff --git a/.github/workflows/release-linux-aarch64.yml b/.github/workflows/release-linux-aarch64.yml index 2d035096b..5725b0f73 100644 --- a/.github/workflows/release-linux-aarch64.yml +++ b/.github/workflows/release-linux-aarch64.yml @@ -227,6 +227,11 @@ jobs: -t "dist/QBZ_${VERSION}_arm64.deb" nfpm package -f packaging/nfpm/nfpm.yaml -p rpm \ -t "dist/QBZ-${VERSION}-1.aarch64.rpm" + # Standalone qbzd packages (daemon-only, mirrors the amd64 job). + nfpm package -f packaging/nfpm/nfpm-qbzd.yaml -p deb \ + -t "dist/qbzd_${VERSION}_arm64.deb" + nfpm package -f packaging/nfpm/nfpm-qbzd.yaml -p rpm \ + -t "dist/qbzd-${VERSION}-1.aarch64.rpm" - name: Build AppImage run: | @@ -328,3 +333,16 @@ jobs: repository: vicrodh/qbz-apt event-type: new-release client-payload: '{"tag": "${{ github.ref_name }}"}' + + # RPM repo (qbz-rpm) — same dual-dispatch rule as #648 above: the arm64 + # .rpms land hours after x86_64's, and update-repo.yml is idempotent. + # continue-on-error until the token scope covers the new repo. + - name: Trigger RPM repo update + if: startsWith(github.ref, 'refs/tags/') + continue-on-error: true + uses: peter-evans/repository-dispatch@v4 + with: + token: ${{ secrets.APT_REPO_TOKEN }} + repository: vicrodh/qbz-rpm + event-type: new-release + client-payload: '{"tag": "${{ github.ref_name }}"}' diff --git a/.github/workflows/release-linux.yml b/.github/workflows/release-linux.yml index 1bb546403..89bd1ca1f 100644 --- a/.github/workflows/release-linux.yml +++ b/.github/workflows/release-linux.yml @@ -262,6 +262,12 @@ jobs: -t "dist/QBZ_${VERSION}_amd64.deb" nfpm package -f packaging/nfpm/nfpm.yaml -p rpm \ -t "dist/QBZ-${VERSION}-1.x86_64.rpm" + # Standalone qbzd packages (daemon-only — the independent deb/rpm + # the owner asked for; rides the same staged ./qbzd). + nfpm package -f packaging/nfpm/nfpm-qbzd.yaml -p deb \ + -t "dist/qbzd_${VERSION}_amd64.deb" + nfpm package -f packaging/nfpm/nfpm-qbzd.yaml -p rpm \ + -t "dist/qbzd-${VERSION}-1.x86_64.rpm" - name: Build AppImage run: | @@ -372,3 +378,16 @@ jobs: repository: vicrodh/qbz-apt event-type: new-release client-payload: '{"tag": "${{ github.ref_name }}"}' + + # Our own RPM repo (qbz-rpm — mirrors qbz-apt's flow with createrepo). + # continue-on-error until the token scope covers the new repo (the PAT + # was minted for qbz-apt only). + - name: Trigger RPM repo update + if: startsWith(github.ref, 'refs/tags/') + continue-on-error: true + uses: peter-evans/repository-dispatch@v4 + with: + token: ${{ secrets.APT_REPO_TOKEN }} + repository: vicrodh/qbz-rpm + event-type: new-release + client-payload: '{"tag": "${{ github.ref_name }}"}' diff --git a/crates/qbz-log/src/redact.rs b/crates/qbz-log/src/redact.rs index 7dff22648..043691d92 100644 --- a/crates/qbz-log/src/redact.rs +++ b/crates/qbz-log/src/redact.rs @@ -78,6 +78,40 @@ fn has_redaction_candidate(lower: &str) -> bool { NEEDLES.iter().any(|n| lower.contains(n)) } +/// IPv4 octet (0-255) — the range guard is what keeps version strings +/// ("1.16.1", "8.2.0") and timestamps out of the mask. +const OCTET: &str = r"(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])"; + +/// IP-masking patterns (owner 2026-07-24): ANY address — LAN or public — is +/// masked to its LAST segment only (`192.168.178.211` → `*.*.*.211`), so +/// uploaded logs stop leaking the user's network topology. The port survives. +/// +/// IPv6 groups must START with a 4-hex-digit group — that is what keeps time +/// strings ("11:41:48.054") and MAC addresses out of the mask (regex has no +/// lookaround, so the width anchor does the disambiguation). +fn ip_patterns() -> &'static Vec { + static PATTERNS: OnceLock> = OnceLock::new(); + PATTERNS.get_or_init(|| { + [ + // IPv4: mask the first three octets, keep the last (capture 1). + format!(r"\b(?:{OCTET}\.){{3}}({OCTET})\b"), + // IPv6 full form (first group 4 hex wide — see the doc above). + r"\b[0-9a-fA-F]{4}:(?:[0-9a-fA-F]{1,4}:){1,6}([0-9a-fA-F]{1,4})\b".to_string(), + // IPv6 `::`-compressed form (same 4-wide anchor). + r"\b[0-9a-fA-F]{4}(?::[0-9a-fA-F]{0,4})*::(?:[0-9a-fA-F]{1,4}:)*([0-9a-fA-F]{1,4})\b".to_string(), + ] + .iter() + .filter_map(|p| Regex::new(p).ok()) + .collect() + }) +} + +/// Cheap pre-check for the IP layer: a dot or colon and a digit, O(n). (The +/// colon is required — IPv6 has no dots.) +fn has_ip_candidate(line: &str) -> bool { + (line.contains('.') || line.contains(':')) && line.bytes().any(|b| b.is_ascii_digit()) +} + /// Redact secrets from a single log line. Literal live-secret layer first, then regex. pub fn redact(line: &str) -> String { let mut out = line.to_string(); @@ -103,6 +137,16 @@ pub fn redact(line: &str) -> String { } } + // Layer 3 — IP masking (`*.*.*.` for v4, `*:*:` for v6). + if has_ip_candidate(&out) { + for (i, re) in ip_patterns().iter().enumerate() { + if re.is_match(&out) { + let replacement = if i == 0 { "*.*.*.${1}" } else { "*:*:${1}" }; + out = re.replace_all(&out, replacement).into_owned(); + } + } + } + out } @@ -141,4 +185,38 @@ mod tests { let r = redact("value abc here"); assert!(r.contains("abc"), "short value should not be scrubbed: {r}"); } + + #[test] + fn masks_ipv4_keep_last_octet() { + let r = redact("DLNA: Set URI to http://192.168.178.211:9876/audio/***/235314822"); + assert_eq!( + r, + "DLNA: Set URI to http://*.*.*.211:9876/audio/***/235314822" + ); + // Public addresses get the same treatment. + let r = redact("dial 203.0.113.9 failed"); + assert!(r.contains("*.*.*.9"), "public IPv4 survived: {r}"); + assert!(!r.contains("203.0.113"), "public IPv4 leaked: {r}"); + } + + #[test] + fn ip_mask_leaves_versions_and_timestamps_alone() { + for s in [ + "2026-07-24 11:41:48.054 INFO qbz", + "bundle 8.2.0-b034", + "slint 1.16.1", + "kernel 6.17.0-40-generic", + "rate 44.1kHz 24-bit / 192 kHz", + ] { + assert_eq!(redact(s), s, "non-IP was mangled: {s}"); + } + } + + #[test] + fn masks_ipv6() { + let r = redact("bind fe80:0000:0000:0000:0224:a4ff:fe12:3456 up"); + assert!(r.contains("*:*:3456"), "full IPv6 survived: {r}"); + let r2 = redact("bind fe80::224:a4ff:fe12:3456 up"); + assert!(r2.contains("*:*:3456"), "compressed IPv6 survived: {r2}"); + } } diff --git a/packaging/gentoo/qbzd-bin.ebuild.template b/packaging/gentoo/qbzd-bin.ebuild.template new file mode 100644 index 000000000..3ffb1a6bf --- /dev/null +++ b/packaging/gentoo/qbzd-bin.ebuild.template @@ -0,0 +1,73 @@ +# Copyright 2024-2026 Gentoo Authors +# Distributed under the terms of the MIT License + +EAPI=8 + +inherit xdg-utils + +DESCRIPTION="QBZ headless Qobuz daemon for servers and appliances (prebuilt binary)" +HOMEPAGE="https://qbz.lol https://github.com/vicrodh/qbz" + +MY_PV="${PV}" +SRC_URI=" + amd64? ( https://github.com/vicrodh/qbz/releases/download/v${MY_PV}/qbzd-${MY_PV}-linux-amd64.tar.gz ) + arm64? ( https://github.com/vicrodh/qbz/releases/download/v${MY_PV}/qbzd-${MY_PV}-linux-aarch64.tar.gz ) +" + +LICENSE="MIT" +SLOT="0" +KEYWORDS="~amd64 ~arm64" +# mirror: GitHub-only distfiles — without this Portage 404s through every +# official Gentoo mirror before reaching SRC_URI. strip: prebuilt binary, +# already stripped at build time; keep Portage's stripper off it. +RESTRICT="mirror strip" + +# qbzd is slint-free: the measured link set is libasound only (+ libc/libm/ +# libgcc from the toolchain). pipewire is the runtime device-enumeration +# helper (bit-perfect sample-rate queries, DAC detection). +RDEPEND=" + media-libs/alsa-lib + media-video/pipewire +" + +BDEPEND="" + +# Binary package, no compilation needed +QA_PREBUILT="usr/bin/qbzd" + +S="${WORKDIR}" + +src_unpack() { + default + # Tarball extracts to qbzd-${PV}-linux-{amd64|aarch64}/ directory +} + +src_install() { + local dir + if use amd64; then + dir="qbzd-${MY_PV}-linux-amd64" + elif use arm64; then + dir="qbzd-${MY_PV}-linux-aarch64" + fi + + dobin "${dir}/qbzd" + + insinto /usr/lib/systemd/user + doins "${dir}/qbzd.service" + + insinto /usr/share/bash-completion/completions + newins "${dir}/completions/qbzd.bash" qbzd + insinto /usr/share/zsh/site-functions + newins "${dir}/completions/qbzd.zsh" _qbzd + insinto /usr/share/fish/vendor_completions.d + newins "${dir}/completions/qbzd.fish" qbzd + + dodoc "${dir}/README.md" +} + +pkg_postinst() { + elog "Run 'qbzd' once for the setup wizard (or 'qbzd tui')." + elog "Optional user service (starts at login):" + elog " systemctl --user enable --now qbzd.service" + elog " loginctl enable-linger \${USER} # keep it running after logout" +} diff --git a/packaging/gentoo/qbzd.ebuild.template b/packaging/gentoo/qbzd.ebuild.template new file mode 100644 index 000000000..b0f9330a7 --- /dev/null +++ b/packaging/gentoo/qbzd.ebuild.template @@ -0,0 +1,79 @@ +# Copyright 2024-2026 Gentoo Authors +# Distributed under the terms of the MIT License + +EAPI=8 + +inherit xdg + +DESCRIPTION="QBZ headless Qobuz daemon for servers and appliances (source build)" +HOMEPAGE="https://qbz.lol https://github.com/vicrodh/qbz" + +MY_PV="${PV}" +SRC_URI="https://github.com/vicrodh/qbz/archive/refs/tags/v${MY_PV}.tar.gz -> ${P}.tar.gz" + +LICENSE="MIT" +SLOT="0" +KEYWORDS="~amd64 ~arm64" +IUSE="pipewire pulseaudio" +RESTRICT="mirror network-sandbox" + +# qbzd is slint-free: the measured link set is libasound only. pipewire / +# pulseaudio are the runtime device-enumeration helpers (bit-perfect +# sample-rate queries and DAC detection, same audio stack as the desktop). +RDEPEND=" + media-libs/alsa-lib + pipewire? ( + media-video/pipewire[pipewire-alsa] + ) + pulseaudio? ( + media-libs/libpulse + ) +" + +BDEPEND=" + dev-lang/rust + virtual/pkgconfig +" + +PDEPEND=" + media-sound/alsa-utils +" + +S="${WORKDIR}/qbz-${MY_PV}" + +src_compile() { + # qbzd is minutes-class (no qbz_ui compilation unit) — the default job + # count is fine here, unlike the desktop package. + cargo build --release \ + --manifest-path crates/Cargo.toml -p qbzd || die "cargo build failed" +} + +src_install() { + dobin "crates/target/release/qbzd" + + # systemd USER unit (opt-in; needs `loginctl enable-linger` to outlive + # the login session). + insinto /usr/lib/systemd/user + doins crates/qbzd/service/qbzd.service + + # Shell completions shipped by the daemon itself. + "crates/target/release/qbzd" completions bash > "${T}/qbzd.bash" || die + "crates/target/release/qbzd" completions zsh > "${T}/_qbzd" || die + "crates/target/release/qbzd" completions fish > "${T}/qbzd.fish" || die + insinto /usr/share/bash-completion/completions + newins "${T}/qbzd.bash" qbzd + insinto /usr/share/zsh/site-functions + newins "${T}/_qbzd" _qbzd + insinto /usr/share/fish/vendor_completions.d + newins "${T}/qbzd.fish" qbzd +} + +pkg_postinst() { + elog "Run 'qbzd' once for the setup wizard (or 'qbzd tui')." + elog "Optional user service (starts at login):" + elog " systemctl --user enable --now qbzd.service" + elog " loginctl enable-linger \${USER} # keep it running after logout" + elog "" + elog "For bit-perfect audio playback, ensure your user is in the 'audio' group:" + elog " usermod -aG audio \${USER}" +} diff --git a/packaging/nfpm/nfpm-qbzd.yaml b/packaging/nfpm/nfpm-qbzd.yaml new file mode 100644 index 000000000..acf373ee3 --- /dev/null +++ b/packaging/nfpm/nfpm-qbzd.yaml @@ -0,0 +1,62 @@ +# nfpm config — builds the STANDALONE qbzd .deb and .rpm (daemon-only, no +# desktop app). Used by release-linux(.yml/-aarch64.yml) alongside the main +# nfpm.yaml; same literal-path rule: the workflow stages ./qbzd before +# invoking nfpm (nfpm does NOT expand env vars in contents[].src). +# +# Interpolated env (exported by the workflow): +# QBZ_VERSION release version without the v prefix (e.g. 2.0.3) +# QBZ_ARCH deb-style arch: amd64 | arm64 +# +# Measured runtime deps of the qbzd binary (ldd, 2026-07-24): libasound only +# (+ libc/libm/libgcc from the toolchain). No fontconfig/freetype/GL — the +# daemon is slint-free. + +name: qbzd +arch: ${QBZ_ARCH} +platform: linux +version: ${QBZ_VERSION} +section: sound +priority: optional +maintainer: Victor Rodriguez +description: | + QBZ headless daemon — Qobuz player for servers, Raspberry Pi and + other appliances. TUI setup wizard, QConnect renderer/receiver, local + library, offline cache. Ships a systemd USER unit (opt-in, disabled by + default; requires `loginctl enable-linger` for boot persistence). +vendor: QBZ +homepage: https://github.com/vicrodh/qbz +license: MIT + +contents: + # LITERAL path (nfpm prints env vars verbatim in contents[].src — the + # workflow places the stripped binary at ./qbzd before invoking nfpm). + - src: ./qbzd + dst: /usr/bin/qbzd + file_info: + mode: 0755 + - src: crates/qbzd/service/qbzd.service + dst: /usr/lib/systemd/user/qbzd.service + +overrides: + deb: + depends: + # t64 alternatives cover Ubuntu 24.04+'s time_t64 renames. + - libasound2 | libasound2t64 + recommends: + # pw-metadata/pw-dump for bit-perfect sample-rate queries and DAC + # detection (same audio stack as the desktop app). + - pipewire + - pulseaudio + rpm: + depends: + - libasound.so.2()(64bit) + recommends: + - pipewire + - pulseaudio + +rpm: + group: Applications/Multimedia + compression: zstd + +deb: + compression: zstd