Compare commits

..
2 Commits
Author SHA1 Message Date
ssmithxandClaude Sonnet 5 be06b3ce2b fix(ui): stop sending an empty ssh_password over the saved router connection
provisionTollgate/saveTollgateConfig/scanWifi/configureWan all fell
back to the Connect form's local refs (host/sshUser/sshPassword) when
connectedParams was null. Those refs only get populated if the form
was actually submitted this session — on a normal page load the
router reconnects via the server-persisted config instead, leaving
sshPassword at its default ''. Sending that as an explicit
(empty-but-present) ssh_password overrides the backend's saved-config
fallback, so every action auths with a blank password instead of the
real saved one.

Added authParams(): omit host/ssh_user/ssh_password entirely unless
connectedParams is actually set, same as the status poll already does.
Caught live: dropbear on archy-x250-pa3's router logged a single bad
password attempt at the exact moment "Install TollGate" was clicked,
sandwiched between periodic status-poll connections succeeding with
the real saved password.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0176RpCxFNS9ZaSJjL72W9Z5
2026-09-05 15:14:07 +00:00
ssmithxandClaude Sonnet 5 f3d96ae2ee fix(openwrt): resolve opkg/apk via $PATH, not a hardcoded /usr/bin path
opkg_check() and every opkg/apk invocation hardcoded /usr/bin/opkg and
/usr/bin/apk. Official OpenWrt images don't all symlink /bin into
/usr/bin — the glinet_gl-mt3000 24.10.2 build keeps them as separate
real directories with opkg living in /bin — so the check silently
missed a perfectly normal install and TollGate provisioning failed
with "this router's firmware may not support package management".

Switched every call to resolve through the router's own $PATH
(command -v / bare opkg / apk) instead. Reproduced and fixed live
against archy-x250-pa3, 2026-09-05.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0176RpCxFNS9ZaSJjL72W9Z5
2026-09-05 15:14:00 +00:00
4 changed files with 44 additions and 34 deletions
+1 -1
View File
@@ -135,7 +135,7 @@ impl RpcHandler {
// not /usr/bin/tollgate-module-basic-go — that's only the opkg/apk // not /usr/bin/tollgate-module-basic-go — that's only the opkg/apk
// *package* name, never an on-disk filename. // *package* name, never an on-disk filename.
let tollgate_installed = router let tollgate_installed = router
.run("/usr/bin/opkg list-installed 2>/dev/null | grep -q '^tollgate-module-basic-go ' || \ .run("opkg list-installed 2>/dev/null | grep -q '^tollgate-module-basic-go ' || \
test -f /usr/bin/tollgate-wrt 2>/dev/null") test -f /usr/bin/tollgate-wrt 2>/dev/null")
.map(|(_, code)| code == 0) .map(|(_, code)| code == 0)
.unwrap_or(false); .unwrap_or(false);
+19 -15
View File
@@ -15,25 +15,32 @@ pub enum PkgManager {
impl Router { impl Router {
/// Detect which package manager is available. /// Detect which package manager is available.
/// ///
/// - If `/usr/bin/opkg` exists → `PkgManager::Opkg` (nothing to do). /// Looks up `opkg`/`apk` via the router's `$PATH` (`command -v`) rather
/// - If `/usr/bin/apk` exists → run `apk update` (switching repos to HTTP /// than a hardcoded `/usr/bin/<tool>` — official OpenWrt images don't all
/// symlink `/bin` into `/usr/bin` (e.g. the `glinet_gl-mt3000` 24.10.2
/// build keeps them as separate real directories with `opkg` living in
/// `/bin`), so a fixed absolute path silently misses a perfectly normal
/// install and reports "no package management" (archy-x250-pa3, 2026-09-05).
///
/// - If `opkg` is on PATH → `PkgManager::Opkg` (nothing to do).
/// - If `apk` is on PATH → run `apk update` (switching repos to HTTP
/// first to work around missing CA bundle on fresh images), then try /// first to work around missing CA bundle on fresh images), then try
/// `apk add opkg`. If opkg is in the repos → `Opkg`. If not (OpenWrt /// `apk add opkg`. If opkg is in the repos → `Opkg`. If not (OpenWrt
/// 25.x) → `ApkNative`. /// 25.x) → `ApkNative`.
/// - Neither found → error. /// - Neither found → error.
pub fn opkg_check(&self) -> Result<PkgManager> { pub fn opkg_check(&self) -> Result<PkgManager> {
let (_, code) = self.run("test -x /usr/bin/opkg")?; let (_, code) = self.run("command -v opkg >/dev/null 2>&1")?;
if code == 0 { if code == 0 {
return Ok(PkgManager::Opkg); return Ok(PkgManager::Opkg);
} }
let (_, apk_code) = self.run("test -x /usr/bin/apk")?; let (_, apk_code) = self.run("command -v apk >/dev/null 2>&1")?;
if apk_code == 0 { if apk_code == 0 {
info!("[{}] opkg not found — using apk (OpenWrt 25.x+)", self.host); info!("[{}] opkg not found — using apk (OpenWrt 25.x+)", self.host);
// Fresh images ship without a CA bundle; switch repos to HTTP so // Fresh images ship without a CA bundle; switch repos to HTTP so
// apk's wget can reach the package index without TLS verification. // apk's wget can reach the package index without TLS verification.
self.run_ok("sed -i 's|https://|http://|g' /etc/apk/repositories 2>/dev/null || true")?; self.run_ok("sed -i 's|https://|http://|g' /etc/apk/repositories 2>/dev/null || true")?;
let (update_out, update_code) = self.run("/usr/bin/apk update 2>&1")?; let (update_out, update_code) = self.run("apk update 2>&1")?;
if update_code != 0 { if update_code != 0 {
anyhow::bail!( anyhow::bail!(
"apk update failed (exit {}) — router may have no internet access. \ "apk update failed (exit {}) — router may have no internet access. \
@@ -43,7 +50,7 @@ impl Router {
); );
} }
// Try to install opkg (only available on some 25.x builds). // Try to install opkg (only available on some 25.x builds).
let (add_out, add_code) = self.run("/usr/bin/apk add opkg 2>&1")?; let (add_out, add_code) = self.run("apk add opkg 2>&1")?;
if add_code == 0 { if add_code == 0 {
return Ok(PkgManager::Opkg); return Ok(PkgManager::Opkg);
} }
@@ -62,7 +69,7 @@ impl Router {
} }
anyhow::bail!( anyhow::bail!(
"opkg not found at /usr/bin/opkg — this router's firmware may not \ "Neither opkg nor apk found on this router's $PATH — its firmware may not \
support package management (TollGate requires a standard OpenWrt build)" support package management (TollGate requires a standard OpenWrt build)"
); );
} }
@@ -70,31 +77,28 @@ impl Router {
/// `opkg update` — refresh package lists. /// `opkg update` — refresh package lists.
pub fn opkg_update(&self) -> Result<()> { pub fn opkg_update(&self) -> Result<()> {
info!("[{}] opkg update", self.host); info!("[{}] opkg update", self.host);
self.run_ok("/usr/bin/opkg update")?; self.run_ok("opkg update")?;
Ok(()) Ok(())
} }
/// Install a package, skipping if already installed. /// Install a package, skipping if already installed.
pub fn opkg_install(&self, package: &str) -> Result<()> { pub fn opkg_install(&self, package: &str) -> Result<()> {
// Check if already installed to avoid unnecessary network traffic. // Check if already installed to avoid unnecessary network traffic.
let (_, code) = self.run(&format!( let (_, code) = self.run(&format!("opkg list-installed | grep -q '^{} '", package))?;
"/usr/bin/opkg list-installed | grep -q '^{} '",
package
))?;
if code == 0 { if code == 0 {
info!("[{}] {} already installed", self.host, package); info!("[{}] {} already installed", self.host, package);
return Ok(()); return Ok(());
} }
info!("[{}] opkg install {}", self.host, package); info!("[{}] opkg install {}", self.host, package);
self.run_ok(&format!("/usr/bin/opkg install {}", package))?; self.run_ok(&format!("opkg install {}", package))?;
Ok(()) Ok(())
} }
/// Remove a package. /// Remove a package.
pub fn opkg_remove(&self, package: &str) -> Result<()> { pub fn opkg_remove(&self, package: &str) -> Result<()> {
info!("[{}] opkg remove {}", self.host, package); info!("[{}] opkg remove {}", self.host, package);
self.run_ok(&format!("/usr/bin/opkg remove {}", package))?; self.run_ok(&format!("opkg remove {}", package))?;
Ok(()) Ok(())
} }
@@ -121,7 +125,7 @@ impl Router {
} }
info!("[{}] apk add {}", self.host, package); info!("[{}] apk add {}", self.host, package);
self.run_ok(&format!("/usr/bin/apk add {}", package))?; self.run_ok(&format!("apk add {}", package))?;
Ok(()) Ok(())
} }
} }
+2 -2
View File
@@ -35,7 +35,7 @@ pub fn install_tollgate(router: &Router) -> Result<()> {
// Package not in any feed — download the .ipk directly. // Package not in any feed — download the .ipk directly.
let arch = router let arch = router
.run_ok("/usr/bin/opkg print-architecture | grep -v all | grep -v noarch | tail -1 | awk '{print $2}'")?; .run_ok("opkg print-architecture | grep -v all | grep -v noarch | tail -1 | awk '{print $2}'")?;
let arch = arch.trim(); let arch = arch.trim();
let url = ipk_url(arch).ok_or_else(|| { let url = ipk_url(arch).ok_or_else(|| {
@@ -88,7 +88,7 @@ pub fn install_tollgate_apk_native(router: &Router) -> Result<()> {
". /etc/openwrt_release 2>/dev/null \ ". /etc/openwrt_release 2>/dev/null \
&& a=\"${DISTRIB_ARCH:-${OPENWRT_ARCH:-}}\" \ && a=\"${DISTRIB_ARCH:-${OPENWRT_ARCH:-}}\" \
&& [ -n \"$a\" ] && echo \"$a\" \ && [ -n \"$a\" ] && echo \"$a\" \
|| /usr/bin/apk --print-arch 2>/dev/null \ || apk --print-arch 2>/dev/null \
|| uname -m", || uname -m",
)?; )?;
// Normalise: uname -m returns bare "mipsel"/"mips"; map to 24kc variant // Normalise: uname -m returns bare "mipsel"/"mips"; map to 24kc variant
+22 -16
View File
@@ -115,6 +115,24 @@ const showConnectForm = ref(false)
const connecting = ref(false) const connecting = ref(false)
const connectedParams = ref<Record<string, string> | null>(null) const connectedParams = ref<Record<string, string> | null>(null)
// Every action below (install/edit TollGate, WiFi scan, WAN configure) needs
// host/ssh_user/ssh_password to reach the router. `connectedParams` only gets
// set when the Connect form was actually submitted this session (WR-03 above)
// — on a normal page load the router reconnects via the server-persisted
// config instead, so `sshPassword`/`sshUser`/`host` (the Connect form's own
// local refs) sit at their untouched defaults ('', 'root', ''). Falling back
// to those refs here used to send an explicit-but-empty ssh_password, which
// the backend treats as "the caller provided this" and never falls back to
// the real saved password — a real router password then fails auth on every
// action even though the status poll (which sends no params at all) keeps
// working fine (archy-x250-pa3, 2026-09-05: dropbear logged one bad-password
// attempt at the exact moment "Install TollGate" was clicked). Omitting the
// fields entirely when there's no explicit connectedParams lets the backend's
// own saved-config fallback do the right thing, same as the status poll.
function authParams(): Record<string, string> {
return connectedParams.value ?? {}
}
const detecting = ref(false) const detecting = ref(false)
const detectError = ref('') const detectError = ref('')
const detectedCandidates = ref<string[]>([]) const detectedCandidates = ref<string[]>([])
@@ -271,11 +289,7 @@ async function provisionTollgate() {
provisionError.value = '' provisionError.value = ''
provisionSuccess.value = false provisionSuccess.value = false
try { try {
const params: Record<string, unknown> = { const params: Record<string, unknown> = { ...authParams() }
host: connectedParams.value?.host ?? status.value?.host,
ssh_user: connectedParams.value?.ssh_user ?? sshUser.value,
ssh_password: connectedParams.value?.ssh_password ?? sshPassword.value,
}
await rpcClient.call({ method: 'openwrt.provision-tollgate', params, timeout: 300000 }) await rpcClient.call({ method: 'openwrt.provision-tollgate', params, timeout: 300000 })
provisionSuccess.value = true provisionSuccess.value = true
await load(connectedParams.value ?? undefined) await load(connectedParams.value ?? undefined)
@@ -302,9 +316,7 @@ async function saveTollgateConfig() {
updateTollgateError.value = '' updateTollgateError.value = ''
try { try {
const params: Record<string, unknown> = { const params: Record<string, unknown> = {
host: connectedParams.value?.host ?? status.value?.host, ...authParams(),
ssh_user: connectedParams.value?.ssh_user ?? sshUser.value,
ssh_password: connectedParams.value?.ssh_password ?? sshPassword.value,
price_sats: editPriceSats.value, price_sats: editPriceSats.value,
step_size_ms: editStepSizeMin.value * 60_000, step_size_ms: editStepSizeMin.value * 60_000,
min_steps: editMinSteps.value, min_steps: editMinSteps.value,
@@ -336,11 +348,7 @@ async function scanWifi() {
wanStep.value = 'scanning' wanStep.value = 'scanning'
wanError.value = '' wanError.value = ''
try { try {
const params: Record<string, unknown> = { const params: Record<string, unknown> = { ...authParams() }
host: connectedParams.value?.host ?? status.value?.host,
ssh_user: connectedParams.value?.ssh_user ?? sshUser.value,
ssh_password: connectedParams.value?.ssh_password ?? sshPassword.value,
}
const result = await rpcClient.call<{ networks: ScannedNetwork[] }>({ const result = await rpcClient.call<{ networks: ScannedNetwork[] }>({
method: 'openwrt.scan-wifi', method: 'openwrt.scan-wifi',
params, params,
@@ -367,9 +375,7 @@ async function configureWan() {
wanError.value = '' wanError.value = ''
try { try {
const params: Record<string, unknown> = { const params: Record<string, unknown> = {
host: connectedParams.value?.host ?? status.value?.host, ...authParams(),
ssh_user: connectedParams.value?.ssh_user ?? sshUser.value,
ssh_password: connectedParams.value?.ssh_password ?? sshPassword.value,
ssid: selectedNetwork.value.ssid, ssid: selectedNetwork.value.ssid,
password: wanPassword.value, password: wanPassword.value,
encryption: selectedNetwork.value.encryption, encryption: selectedNetwork.value.encryption,