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 -> *.*.*.<last>, IPv6 -> *:*:<last>, with
  version/timestamp false-positive guards (qbz-log 10/10 tests).

km
This commit is contained in:
vicrodh committed 2026-07-25 12:05:56 -06:00
1 parent a8919cbadf
commit efa486ba4f
6 files changed
+329

No files matched your search

@@ -227,6 +227,11 @@ jobs:
-t "dist/QBZ_${VERSION}_arm64.deb" -t "dist/QBZ_${VERSION}_arm64.deb"
nfpm package -f packaging/nfpm/nfpm.yaml -p rpm \ nfpm package -f packaging/nfpm/nfpm.yaml -p rpm \
-t "dist/QBZ-${VERSION}-1.aarch64.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 - name: Build AppImage
run: | run: |
@@ -328,3 +333,16 @@ jobs:
repository: vicrodh/qbz-apt repository: vicrodh/qbz-apt
event-type: new-release event-type: new-release
client-payload: '{"tag": "${{ github.ref_name }}"}' 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 }}"}'
+19
View File
@@ -262,6 +262,12 @@ jobs:
-t "dist/QBZ_${VERSION}_amd64.deb" -t "dist/QBZ_${VERSION}_amd64.deb"
nfpm package -f packaging/nfpm/nfpm.yaml -p rpm \ nfpm package -f packaging/nfpm/nfpm.yaml -p rpm \
-t "dist/QBZ-${VERSION}-1.x86_64.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 - name: Build AppImage
run: | run: |
@@ -372,3 +378,16 @@ jobs:
repository: vicrodh/qbz-apt repository: vicrodh/qbz-apt
event-type: new-release event-type: new-release
client-payload: '{"tag": "${{ github.ref_name }}"}' 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 }}"}'
+78
View File
@@ -78,6 +78,40 @@ fn has_redaction_candidate(lower: &str) -> bool {
NEEDLES.iter().any(|n| lower.contains(n)) 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<Regex> {
static PATTERNS: OnceLock<Vec<Regex>> = 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. /// Redact secrets from a single log line. Literal live-secret layer first, then regex.
pub fn redact(line: &str) -> String { pub fn redact(line: &str) -> String {
let mut out = line.to_string(); let mut out = line.to_string();
@@ -103,6 +137,16 @@ pub fn redact(line: &str) -> String {
} }
} }
// Layer 3 — IP masking (`*.*.*.<last>` for v4, `*:*:<last>` 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 out
} }
@@ -141,4 +185,38 @@ mod tests {
let r = redact("value abc here"); let r = redact("value abc here");
assert!(r.contains("abc"), "short value should not be scrubbed: {r}"); 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}");
}
} }
+73
View File
@@ -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"
}
+79
View File
@@ -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}"
}
+62
View File
@@ -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 <vicrodh@users.noreply.github.com>
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