fix(quadlet): escape % for systemd specifiers — bitcoind RPC creds were /bin/bash
The creds-off-argv script used printf "rpcuser=%s" in the manifest's custom_args; quadlet copies Exec= into the generated service's ExecStart, where systemd expands %s (user's shell) at load time — bitcoind's rpc.conf came out as rpcuser=/bin/bash and every RPC consumer got 401s on quadlet nodes (framework-pt: bitcoin-status, HA block-height sensor, LND chain RPC). Two-layer fix: quadlet.rs now escapes % -> %% in Exec/Entrypoint/HealthCmd/ Environment emission (with regression test), and the bitcoin manifests write rpc.conf with plain echo so the catalog also heals nodes still running older binaries. Release catalog regenerated with the fixed scripts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
317a72aafe
commit
745e180102
@ -37,7 +37,7 @@ app:
|
|||||||
RPC_PASS="$(printenv BITCOIN_RPC_PASS)";
|
RPC_PASS="$(printenv BITCOIN_RPC_PASS)";
|
||||||
RPC_CONF="/tmp/rpc.conf";
|
RPC_CONF="/tmp/rpc.conf";
|
||||||
umask 077;
|
umask 077;
|
||||||
printf "rpcuser=%s\nrpcpassword=%s\n" "$RPC_USER" "$RPC_PASS" > "$RPC_CONF";
|
{ echo "rpcuser=$RPC_USER"; echo "rpcpassword=$RPC_PASS"; } > "$RPC_CONF";
|
||||||
RPC_TXRELAY_AUTH="$(printenv BITCOIN_RPC_TXRELAY_RPCAUTH || true)";
|
RPC_TXRELAY_AUTH="$(printenv BITCOIN_RPC_TXRELAY_RPCAUTH || true)";
|
||||||
DISK_GB_VALUE="$(printenv DISK_GB || true)";
|
DISK_GB_VALUE="$(printenv DISK_GB || true)";
|
||||||
RPC_HEADROOM="-rpcthreads=16 -rpcworkqueue=256";
|
RPC_HEADROOM="-rpcthreads=16 -rpcworkqueue=256";
|
||||||
|
|||||||
@ -37,7 +37,7 @@ app:
|
|||||||
RPC_PASS="$(printenv BITCOIN_RPC_PASS)";
|
RPC_PASS="$(printenv BITCOIN_RPC_PASS)";
|
||||||
RPC_CONF="/tmp/rpc.conf";
|
RPC_CONF="/tmp/rpc.conf";
|
||||||
umask 077;
|
umask 077;
|
||||||
printf "rpcuser=%s\nrpcpassword=%s\n" "$RPC_USER" "$RPC_PASS" > "$RPC_CONF";
|
{ echo "rpcuser=$RPC_USER"; echo "rpcpassword=$RPC_PASS"; } > "$RPC_CONF";
|
||||||
RPC_TXRELAY_AUTH="$(printenv BITCOIN_RPC_TXRELAY_RPCAUTH || true)";
|
RPC_TXRELAY_AUTH="$(printenv BITCOIN_RPC_TXRELAY_RPCAUTH || true)";
|
||||||
DISK_GB_VALUE="$(printenv DISK_GB || true)";
|
DISK_GB_VALUE="$(printenv DISK_GB || true)";
|
||||||
RPC_HEADROOM="-rpcthreads=16 -rpcworkqueue=256";
|
RPC_HEADROOM="-rpcthreads=16 -rpcworkqueue=256";
|
||||||
|
|||||||
@ -284,7 +284,7 @@ impl QuadletUnit {
|
|||||||
let _ = writeln!(s, "PodmanArgs=--cpus={cpus}");
|
let _ = writeln!(s, "PodmanArgs=--cpus={cpus}");
|
||||||
}
|
}
|
||||||
if let Some(h) = &self.health {
|
if let Some(h) = &self.health {
|
||||||
let _ = writeln!(s, "HealthCmd={}", h.cmd);
|
let _ = writeln!(s, "HealthCmd={}", h.cmd.replace('%', "%%"));
|
||||||
let _ = writeln!(s, "HealthInterval={}", h.interval);
|
let _ = writeln!(s, "HealthInterval={}", h.interval);
|
||||||
let _ = writeln!(s, "HealthTimeout={}", h.timeout);
|
let _ = writeln!(s, "HealthTimeout={}", h.timeout);
|
||||||
let _ = writeln!(s, "HealthRetries={}", h.retries);
|
let _ = writeln!(s, "HealthRetries={}", h.retries);
|
||||||
@ -298,7 +298,7 @@ impl QuadletUnit {
|
|||||||
// images use `ENTRYPOINT ["bitcoind"]`, which turned the wrapper
|
// images use `ENTRYPOINT ["bitcoind"]`, which turned the wrapper
|
||||||
// into `bitcoind sh -lc ...` and crash-looped). Emitting
|
// into `bitcoind sh -lc ...` and crash-looped). Emitting
|
||||||
// Entrypoint= makes the unit independent of the image's entrypoint.
|
// Entrypoint= makes the unit independent of the image's entrypoint.
|
||||||
let _ = writeln!(s, "Entrypoint={first}");
|
let _ = writeln!(s, "Entrypoint={}", first.replace('%', "%%"));
|
||||||
let mut parts: Vec<String> = rest.to_vec();
|
let mut parts: Vec<String> = rest.to_vec();
|
||||||
parts.extend(self.command.iter().cloned());
|
parts.extend(self.command.iter().cloned());
|
||||||
if !parts.is_empty() {
|
if !parts.is_empty() {
|
||||||
@ -335,11 +335,16 @@ impl QuadletUnit {
|
|||||||
/// the minimum quoting needed so quadlet's parser sees one element per
|
/// the minimum quoting needed so quadlet's parser sees one element per
|
||||||
/// item: anything containing whitespace, quotes, or shell metacharacters
|
/// item: anything containing whitespace, quotes, or shell metacharacters
|
||||||
/// gets wrapped in double quotes with embedded `"` and `\` escaped.
|
/// gets wrapped in double quotes with embedded `"` and `\` escaped.
|
||||||
|
///
|
||||||
|
/// `%` is escaped to `%%`: quadlet copies Exec= content into the generated
|
||||||
|
/// service's ExecStart, where systemd expands `%` specifiers at load time —
|
||||||
|
/// a bare `%s` in a manifest script silently became `/bin/bash` (the user's
|
||||||
|
/// shell) and corrupted bitcoind's generated rpc.conf.
|
||||||
fn shell_join(parts: &[String]) -> String {
|
fn shell_join(parts: &[String]) -> String {
|
||||||
parts
|
parts
|
||||||
.iter()
|
.iter()
|
||||||
.map(|p| {
|
.map(|p| {
|
||||||
let p = p.replace(['\r', '\n'], " ");
|
let p = p.replace(['\r', '\n'], " ").replace('%', "%%");
|
||||||
if p.is_empty() || p.chars().any(|c| c.is_whitespace() || "\"\\$`".contains(c)) {
|
if p.is_empty() || p.chars().any(|c| c.is_whitespace() || "\"\\$`".contains(c)) {
|
||||||
let escaped = p
|
let escaped = p
|
||||||
.replace('\\', "\\\\")
|
.replace('\\', "\\\\")
|
||||||
@ -355,7 +360,8 @@ fn shell_join(parts: &[String]) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn quote_environment(env: &str) -> String {
|
fn quote_environment(env: &str) -> String {
|
||||||
let env = env.replace(['\r', '\n'], " ");
|
// `%` → `%%` for the same systemd-specifier reason as shell_join.
|
||||||
|
let env = env.replace(['\r', '\n'], " ").replace('%', "%%");
|
||||||
if env.is_empty()
|
if env.is_empty()
|
||||||
|| env
|
|| env
|
||||||
.chars()
|
.chars()
|
||||||
@ -1122,6 +1128,20 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn percent_is_escaped_for_systemd_specifiers() {
|
||||||
|
// Regression: a bare `%s` in an Exec= line is a systemd specifier
|
||||||
|
// (user's shell = /bin/bash) once quadlet copies it into the
|
||||||
|
// generated ExecStart — bitcoind's rpc.conf came out as
|
||||||
|
// `rpcuser=/bin/bash` and every RPC consumer got 401s.
|
||||||
|
assert_eq!(
|
||||||
|
shell_join(&["printf 'rpcuser=%s' \"$U\"".to_string()]),
|
||||||
|
"\"printf 'rpcuser=%%s' \\\"$$U\\\"\""
|
||||||
|
);
|
||||||
|
assert_eq!(shell_join(&["--fmt=%h:%p".to_string()]), "--fmt=%%h:%%p");
|
||||||
|
assert_eq!(quote_environment("FMT=%m"), "FMT=%%m");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn restart_policy_emits_correct_systemd_string() {
|
fn restart_policy_emits_correct_systemd_string() {
|
||||||
assert_eq!(RestartPolicy::Always.as_systemd(), "always");
|
assert_eq!(RestartPolicy::Always.as_systemd(), "always");
|
||||||
|
|||||||
@ -423,7 +423,7 @@
|
|||||||
"-lc"
|
"-lc"
|
||||||
],
|
],
|
||||||
"custom_args": [
|
"custom_args": [
|
||||||
"BITCOIND=\"$(command -v bitcoind || true)\"; if [ -z \"$BITCOIND\" ]; then\n BITCOIND=\"$(find /opt -path '*/bin/bitcoind' -type f 2>/dev/null | sort | tail -n 1)\";\nfi; if [ -z \"$BITCOIND\" ]; then\n echo \"bitcoind not found in image\" >&2;\n exit 127;\nfi; RPC_USER=\"$(printenv BITCOIN_RPC_USER)\"; RPC_PASS=\"$(printenv BITCOIN_RPC_PASS)\"; RPC_CONF=\"/tmp/rpc.conf\"; umask 077; printf \"rpcuser=%s\\nrpcpassword=%s\\n\" \"$RPC_USER\" \"$RPC_PASS\" > \"$RPC_CONF\"; RPC_TXRELAY_AUTH=\"$(printenv BITCOIN_RPC_TXRELAY_RPCAUTH || true)\"; DISK_GB_VALUE=\"$(printenv DISK_GB || true)\"; RPC_HEADROOM=\"-rpcthreads=16 -rpcworkqueue=256\"; RPC_TXRELAY_FLAGS=\"-rpcwhitelistdefault=0\"; if [ -n \"$RPC_TXRELAY_AUTH\" ]; then\n RPC_TXRELAY_FLAGS=\"$RPC_TXRELAY_FLAGS -rpcauth=$RPC_TXRELAY_AUTH -rpcwhitelist=txrelay:sendrawtransaction,submitpackage,testmempoolaccept,getmempoolinfo,getrawmempool,getmempoolentry,getnetworkinfo,getblockchaininfo,getblockcount,getblockhash,getblock,getblockheader,getrawtransaction,gettxout,gettxspendingprevout,decoderawtransaction,decodescript,estimatesmartfee,uptime,ping,getconnectioncount,getpeerinfo,getindexinfo,getdeploymentinfo,getchaintips\";\nfi; if [ \"${DISK_GB_VALUE:-0}\" -lt 1000 ]; then\n exec \"$BITCOIND\" -datadir=/home/bitcoin/.bitcoin -conf=\"$RPC_CONF\" -printtoconsole=0 -server=1 -prune=550 -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0:8332 -listen=1 -bind=0.0.0.0:8333 -dbcache=1024 -par=0 -maxconnections=125 $RPC_HEADROOM $RPC_TXRELAY_FLAGS;\nelse\n exec \"$BITCOIND\" -datadir=/home/bitcoin/.bitcoin -conf=\"$RPC_CONF\" -printtoconsole=0 -server=1 -txindex=1 -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0:8332 -listen=1 -bind=0.0.0.0:8333 -dbcache=4096 -par=0 -maxconnections=125 $RPC_HEADROOM $RPC_TXRELAY_FLAGS;\nfi"
|
"BITCOIND=\"$(command -v bitcoind || true)\"; if [ -z \"$BITCOIND\" ]; then\n BITCOIND=\"$(find /opt -path '*/bin/bitcoind' -type f 2>/dev/null | sort | tail -n 1)\";\nfi; if [ -z \"$BITCOIND\" ]; then\n echo \"bitcoind not found in image\" >&2;\n exit 127;\nfi; RPC_USER=\"$(printenv BITCOIN_RPC_USER)\"; RPC_PASS=\"$(printenv BITCOIN_RPC_PASS)\"; RPC_CONF=\"/tmp/rpc.conf\"; umask 077; { echo \"rpcuser=$RPC_USER\"; echo \"rpcpassword=$RPC_PASS\"; } > \"$RPC_CONF\"; RPC_TXRELAY_AUTH=\"$(printenv BITCOIN_RPC_TXRELAY_RPCAUTH || true)\"; DISK_GB_VALUE=\"$(printenv DISK_GB || true)\"; RPC_HEADROOM=\"-rpcthreads=16 -rpcworkqueue=256\"; RPC_TXRELAY_FLAGS=\"-rpcwhitelistdefault=0\"; if [ -n \"$RPC_TXRELAY_AUTH\" ]; then\n RPC_TXRELAY_FLAGS=\"$RPC_TXRELAY_FLAGS -rpcauth=$RPC_TXRELAY_AUTH -rpcwhitelist=txrelay:sendrawtransaction,submitpackage,testmempoolaccept,getmempoolinfo,getrawmempool,getmempoolentry,getnetworkinfo,getblockchaininfo,getblockcount,getblockhash,getblock,getblockheader,getrawtransaction,gettxout,gettxspendingprevout,decoderawtransaction,decodescript,estimatesmartfee,uptime,ping,getconnectioncount,getpeerinfo,getindexinfo,getdeploymentinfo,getchaintips\";\nfi; if [ \"${DISK_GB_VALUE:-0}\" -lt 1000 ]; then\n exec \"$BITCOIND\" -datadir=/home/bitcoin/.bitcoin -conf=\"$RPC_CONF\" -printtoconsole=0 -server=1 -prune=550 -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0:8332 -listen=1 -bind=0.0.0.0:8333 -dbcache=1024 -par=0 -maxconnections=125 $RPC_HEADROOM $RPC_TXRELAY_FLAGS;\nelse\n exec \"$BITCOIND\" -datadir=/home/bitcoin/.bitcoin -conf=\"$RPC_CONF\" -printtoconsole=0 -server=1 -txindex=1 -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0:8332 -listen=1 -bind=0.0.0.0:8333 -dbcache=4096 -par=0 -maxconnections=125 $RPC_HEADROOM $RPC_TXRELAY_FLAGS;\nfi"
|
||||||
],
|
],
|
||||||
"derived_env": [
|
"derived_env": [
|
||||||
{
|
{
|
||||||
@ -566,7 +566,7 @@
|
|||||||
"-lc"
|
"-lc"
|
||||||
],
|
],
|
||||||
"custom_args": [
|
"custom_args": [
|
||||||
"BITCOIND=\"$(command -v bitcoind || true)\"; if [ -z \"$BITCOIND\" ]; then\n BITCOIND=\"$(find /opt -path '*/bin/bitcoind' -type f 2>/dev/null | sort | tail -n 1)\";\nfi; if [ -z \"$BITCOIND\" ]; then\n echo \"bitcoind not found in image\" >&2;\n exit 127;\nfi; RPC_USER=\"$(printenv BITCOIN_RPC_USER)\"; RPC_PASS=\"$(printenv BITCOIN_RPC_PASS)\"; RPC_CONF=\"/tmp/rpc.conf\"; umask 077; printf \"rpcuser=%s\\nrpcpassword=%s\\n\" \"$RPC_USER\" \"$RPC_PASS\" > \"$RPC_CONF\"; RPC_TXRELAY_AUTH=\"$(printenv BITCOIN_RPC_TXRELAY_RPCAUTH || true)\"; DISK_GB_VALUE=\"$(printenv DISK_GB || true)\"; RPC_HEADROOM=\"-rpcthreads=16 -rpcworkqueue=256\"; RPC_TXRELAY_FLAGS=\"-rpcwhitelistdefault=0\"; if [ -n \"$RPC_TXRELAY_AUTH\" ]; then\n RPC_TXRELAY_FLAGS=\"$RPC_TXRELAY_FLAGS -rpcauth=$RPC_TXRELAY_AUTH -rpcwhitelist=txrelay:sendrawtransaction,submitpackage,testmempoolaccept,getmempoolinfo,getrawmempool,getmempoolentry,getnetworkinfo,getblockchaininfo,getblockcount,getblockhash,getblock,getblockheader,getrawtransaction,gettxout,gettxspendingprevout,decoderawtransaction,decodescript,estimatesmartfee,uptime,ping,getconnectioncount,getpeerinfo,getindexinfo,getdeploymentinfo,getchaintips\";\nfi; if [ \"${DISK_GB_VALUE:-0}\" -lt 1000 ]; then\n exec \"$BITCOIND\" -datadir=/home/bitcoin/.bitcoin -conf=\"$RPC_CONF\" -printtoconsole=0 -server=1 -prune=550 -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0:8332 -listen=1 -bind=0.0.0.0:8333 -dbcache=2048 -par=0 -maxconnections=125 $RPC_HEADROOM $RPC_TXRELAY_FLAGS;\nelse\n exec \"$BITCOIND\" -datadir=/home/bitcoin/.bitcoin -conf=\"$RPC_CONF\" -printtoconsole=0 -server=1 -txindex=1 -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0:8332 -listen=1 -bind=0.0.0.0:8333 -dbcache=4096 -par=0 -maxconnections=125 $RPC_HEADROOM $RPC_TXRELAY_FLAGS;\nfi"
|
"BITCOIND=\"$(command -v bitcoind || true)\"; if [ -z \"$BITCOIND\" ]; then\n BITCOIND=\"$(find /opt -path '*/bin/bitcoind' -type f 2>/dev/null | sort | tail -n 1)\";\nfi; if [ -z \"$BITCOIND\" ]; then\n echo \"bitcoind not found in image\" >&2;\n exit 127;\nfi; RPC_USER=\"$(printenv BITCOIN_RPC_USER)\"; RPC_PASS=\"$(printenv BITCOIN_RPC_PASS)\"; RPC_CONF=\"/tmp/rpc.conf\"; umask 077; { echo \"rpcuser=$RPC_USER\"; echo \"rpcpassword=$RPC_PASS\"; } > \"$RPC_CONF\"; RPC_TXRELAY_AUTH=\"$(printenv BITCOIN_RPC_TXRELAY_RPCAUTH || true)\"; DISK_GB_VALUE=\"$(printenv DISK_GB || true)\"; RPC_HEADROOM=\"-rpcthreads=16 -rpcworkqueue=256\"; RPC_TXRELAY_FLAGS=\"-rpcwhitelistdefault=0\"; if [ -n \"$RPC_TXRELAY_AUTH\" ]; then\n RPC_TXRELAY_FLAGS=\"$RPC_TXRELAY_FLAGS -rpcauth=$RPC_TXRELAY_AUTH -rpcwhitelist=txrelay:sendrawtransaction,submitpackage,testmempoolaccept,getmempoolinfo,getrawmempool,getmempoolentry,getnetworkinfo,getblockchaininfo,getblockcount,getblockhash,getblock,getblockheader,getrawtransaction,gettxout,gettxspendingprevout,decoderawtransaction,decodescript,estimatesmartfee,uptime,ping,getconnectioncount,getpeerinfo,getindexinfo,getdeploymentinfo,getchaintips\";\nfi; if [ \"${DISK_GB_VALUE:-0}\" -lt 1000 ]; then\n exec \"$BITCOIND\" -datadir=/home/bitcoin/.bitcoin -conf=\"$RPC_CONF\" -printtoconsole=0 -server=1 -prune=550 -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0:8332 -listen=1 -bind=0.0.0.0:8333 -dbcache=2048 -par=0 -maxconnections=125 $RPC_HEADROOM $RPC_TXRELAY_FLAGS;\nelse\n exec \"$BITCOIND\" -datadir=/home/bitcoin/.bitcoin -conf=\"$RPC_CONF\" -printtoconsole=0 -server=1 -txindex=1 -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0:8332 -listen=1 -bind=0.0.0.0:8333 -dbcache=4096 -par=0 -maxconnections=125 $RPC_HEADROOM $RPC_TXRELAY_FLAGS;\nfi"
|
||||||
],
|
],
|
||||||
"derived_env": [
|
"derived_env": [
|
||||||
{
|
{
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user