The link both projects wrote twice
dev-updater serves APKs to a phone and ai-app runs model sessions for one. Above the waterline they share nothing. Underneath they are the same program: bound to wg0 so they are not on the LAN, presenting a certificate from a CA the app pins, answering only requests carrying a token enrolled by scanning a QR off the terminal, keeping state in owner-only files outside the repo. Three modules, each extracted only after diffing the two copies and finding nothing between them but a product name and a type parameter. netif fails closed when the tunnel is down. enroll generates, stores and compares the token, and prints the QR, with the URI scheme as the one per-project part. private owns the file modes, taken from ai-app's version because it had already factored out what dev-updater still has inline in two places. Nothing is removed from either project. This is a proposal with a working core, and the README carries the measured evidence -- the enrollment scanner activity differs by its package line and nothing else, the RON format module is byte-identical, and the two copies have each drifted into holding an improvement the other lacks, which is the cost being paid today.
This commit is contained in:
1 parent
995b29f10d
commit
7651d491ac
9 files changed
+871
No files matched your search
@@ -0,0 +1,111 @@
|
||||
//! Which address to bind, and which addresses the certificate must cover.
|
||||
//!
|
||||
//! Both projects bind the WireGuard interface and nothing else, so that
|
||||
//! neither is reachable from the LAN. That is the outer of two gates --
|
||||
//! the tunnel decides who can try, the token (see [`crate::enroll`])
|
||||
//! decides who is answered -- and it is worth having on its own account:
|
||||
//! an unenrolled scanner never reaches the token check, and a plain-HTTP
|
||||
//! bootstrap port travels inside the tunnel's encryption.
|
||||
|
||||
use std::net::IpAddr;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
/// The interface both projects bind. A constant rather than a parameter
|
||||
/// because a second answer would mean two ideas of what "the tunnel" is.
|
||||
pub const WG_INTERFACE: &str = "wg0";
|
||||
|
||||
/// The alias an Android emulator reaches its host by. Not a real
|
||||
/// interface anywhere, which is why it has to be added by hand.
|
||||
const EMULATOR_HOST_ALIAS: [u8; 4] = [10, 0, 2, 2];
|
||||
|
||||
/// Every address this machine answers on, for the leaf certificate's SANs
|
||||
/// -- so it covers whatever the phone actually dials without anyone
|
||||
/// maintaining a hardcoded IP.
|
||||
///
|
||||
/// Loopback is included for curl and tests, and the emulator's host alias
|
||||
/// so a debug build can reach a server running beside it.
|
||||
///
|
||||
/// Failing to enumerate is not fatal: the certificate still covers
|
||||
/// loopback, which is enough to start and to diagnose from the machine
|
||||
/// itself.
|
||||
pub fn local_addresses() -> Vec<IpAddr> {
|
||||
let mut addresses = vec![
|
||||
IpAddr::from([127, 0, 0, 1]),
|
||||
IpAddr::from(EMULATOR_HOST_ALIAS),
|
||||
];
|
||||
match if_addrs::get_if_addrs() {
|
||||
Ok(interfaces) => {
|
||||
for interface in interfaces {
|
||||
let ip = interface.ip();
|
||||
if ip.is_ipv4() && !addresses.contains(&ip) {
|
||||
addresses.push(ip);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => tracing::warn!("couldn't enumerate interfaces for the certificate: {err}"),
|
||||
}
|
||||
addresses
|
||||
}
|
||||
|
||||
/// The IPv4 address on the WireGuard interface, or a refusal to start.
|
||||
///
|
||||
/// Failing closed rather than falling back to 0.0.0.0 is the point. The
|
||||
/// escape hatch belongs to the caller as an explicit `--bind`, because
|
||||
/// each of these servers is also how something stranded gets recovered,
|
||||
/// and that recovery should not depend on the tunnel being healthy.
|
||||
///
|
||||
/// `product` names the binary in the failure, so the message reads as
|
||||
/// advice rather than as a library complaining.
|
||||
pub fn wg_address(product: &str) -> Result<IpAddr> {
|
||||
let interfaces = if_addrs::get_if_addrs().context("enumerate network interfaces")?;
|
||||
interfaces
|
||||
.into_iter()
|
||||
.find(|iface| iface.name == WG_INTERFACE && iface.ip().is_ipv4())
|
||||
.map(|iface| iface.ip())
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"no IPv4 address on interface {WG_INTERFACE} -- {product} binds only to the \
|
||||
WireGuard tunnel, so that only enrolled peers can reach its API. Bring the \
|
||||
tunnel up, or pass --bind 0.0.0.0 to serve the LAN while recovering."
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Whatever this machine has, the two that are not interfaces must be
|
||||
/// there -- loopback for tests and curl, the alias for an emulator --
|
||||
/// and nothing may appear twice, since these become certificate SANs.
|
||||
#[test]
|
||||
fn the_certificate_always_covers_loopback_and_the_emulator_alias() {
|
||||
let addresses = local_addresses();
|
||||
assert!(addresses.contains(&IpAddr::from([127, 0, 0, 1])));
|
||||
assert!(addresses.contains(&IpAddr::from(EMULATOR_HOST_ALIAS)));
|
||||
|
||||
let mut seen = addresses.clone();
|
||||
seen.sort();
|
||||
seen.dedup();
|
||||
assert_eq!(seen.len(), addresses.len(), "duplicate SANs: {addresses:?}");
|
||||
assert!(addresses.iter().all(|ip| ip.is_ipv4()));
|
||||
}
|
||||
|
||||
/// The failure is the thing a person reads at 2am, so it has to name
|
||||
/// the binary, the interface, and the way out.
|
||||
#[test]
|
||||
fn a_missing_tunnel_explains_itself() {
|
||||
// Only meaningful where there is no wg0; where there is one, the
|
||||
// call succeeds and there is no message to check.
|
||||
if wg_address("demo-server").is_ok() {
|
||||
return;
|
||||
}
|
||||
let err = wg_address("demo-server")
|
||||
.expect_err("no tunnel")
|
||||
.to_string();
|
||||
assert!(err.contains("demo-server"), "{err}");
|
||||
assert!(err.contains(WG_INTERFACE), "{err}");
|
||||
assert!(err.contains("--bind"), "{err}");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user