New `archipelago-openwrt` workspace crate provides SSH/UCI-based management of OpenWrt routers, including automated TollGate installation and configuration of a pay-as-you-go "archipelago" SSID backed by the local Cashu mint. Exposes two RPC endpoints: - `openwrt.scan` — discover OpenWrt routers on the LAN - `openwrt.provision-tollgate` — install tollgate-module-basic-go, write UCI config (TIP-01/TIP-02), and create isolated WiFi SSID + firewall zone Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
88 lines
2.9 KiB
Rust
88 lines
2.9 KiB
Rust
use anyhow::{Context, Result};
|
|
use ssh2::Session;
|
|
use std::io::Read;
|
|
use std::net::TcpStream;
|
|
use std::path::Path;
|
|
use tracing::debug;
|
|
|
|
/// An active SSH connection to an OpenWrt router.
|
|
pub struct Router {
|
|
pub host: String,
|
|
pub port: u16,
|
|
session: Session,
|
|
}
|
|
|
|
impl Router {
|
|
/// Connect to an OpenWrt router via SSH using a private key.
|
|
pub fn connect(host: &str, port: u16, user: &str, key_path: &Path) -> Result<Self> {
|
|
let addr = format!("{}:{}", host, port);
|
|
let tcp = TcpStream::connect(&addr)
|
|
.with_context(|| format!("TCP connect to {}", addr))?;
|
|
|
|
let mut session = Session::new().context("create SSH session")?;
|
|
session.set_tcp_stream(tcp);
|
|
session.handshake().context("SSH handshake")?;
|
|
session
|
|
.userauth_pubkey_file(user, None, key_path, None)
|
|
.with_context(|| format!("SSH auth as {} with key {:?}", user, key_path))?;
|
|
|
|
Ok(Self {
|
|
host: host.to_string(),
|
|
port,
|
|
session,
|
|
})
|
|
}
|
|
|
|
/// Connect using a password (fallback for routers not yet provisioned with a key).
|
|
pub fn connect_password(host: &str, port: u16, user: &str, password: &str) -> Result<Self> {
|
|
let addr = format!("{}:{}", host, port);
|
|
let tcp = TcpStream::connect(&addr)
|
|
.with_context(|| format!("TCP connect to {}", addr))?;
|
|
|
|
let mut session = Session::new().context("create SSH session")?;
|
|
session.set_tcp_stream(tcp);
|
|
session.handshake().context("SSH handshake")?;
|
|
session
|
|
.userauth_password(user, password)
|
|
.with_context(|| format!("SSH password auth as {}", user))?;
|
|
|
|
Ok(Self {
|
|
host: host.to_string(),
|
|
port,
|
|
session,
|
|
})
|
|
}
|
|
|
|
/// Run a command and return (stdout, exit_code).
|
|
pub fn run(&self, cmd: &str) -> Result<(String, i32)> {
|
|
debug!("ssh [{}] $ {}", self.host, cmd);
|
|
|
|
let mut channel = self.session.channel_session().context("open channel")?;
|
|
channel.exec(cmd).with_context(|| format!("exec: {}", cmd))?;
|
|
|
|
let mut stdout = String::new();
|
|
channel.read_to_string(&mut stdout).context("read stdout")?;
|
|
channel.wait_close().context("wait close")?;
|
|
let exit = channel.exit_status().context("exit status")?;
|
|
|
|
Ok((stdout, exit))
|
|
}
|
|
|
|
/// Run a command, fail if exit code is non-zero.
|
|
pub fn run_ok(&self, cmd: &str) -> Result<String> {
|
|
let (out, code) = self.run(cmd)?;
|
|
if code != 0 {
|
|
anyhow::bail!("command `{}` exited with code {}: {}", cmd, code, out.trim());
|
|
}
|
|
Ok(out)
|
|
}
|
|
|
|
/// Verify the remote device is actually running OpenWrt.
|
|
pub fn verify_openwrt(&self) -> Result<String> {
|
|
let release = self
|
|
.run_ok("cat /etc/openwrt_release")
|
|
.context("read /etc/openwrt_release — is this an OpenWrt device?")?;
|
|
Ok(release)
|
|
}
|
|
}
|