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:
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 }}"}'
|
||||||
@@ -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,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}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -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}"
|
||||||
|
}
|
||||||
@@ -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
|
||||||
Reference in new issue
Block a user