| 221 |
221 |
|
///
|
| 222 |
222 |
|
/// Returns the trimmed value, or `None` for the vendor's control plane, which
|
| 223 |
223 |
|
/// is what an empty field means.
|
|
224 |
+ |
/// Is there a route off this machine, ignoring the mesh's own interface?
|
|
225 |
+ |
///
|
|
226 |
+ |
/// `tailscale up` reaches a control server before it can print anything. With
|
|
227 |
+ |
/// no route it does not fail, it retries: the console suspends, the user is
|
|
228 |
+ |
/// handed a clean terminal, and nothing appears on it. Measured on fw12,
|
|
229 |
+ |
/// 2026-09-07, after a reinstall took the Wi-Fi credentials with everything
|
|
230 |
+ |
/// else. That is the same shape as the bug `usr/bin/alloy-mesh-up` exists to
|
|
231 |
+ |
/// close and it fails in the same expensive place, *after* the password prompt,
|
|
232 |
+ |
/// one layer further out.
|
|
233 |
+ |
///
|
|
234 |
+ |
/// A default route rather than a reachability probe. What is being answered is
|
|
235 |
+ |
/// "is it worth handing over the terminal", and a machine with no default route
|
|
236 |
+ |
/// certainly is not; a machine that has one and still cannot reach the control
|
|
237 |
+ |
/// plane has a problem `tailscale up` reports better than a preflight would.
|
|
238 |
+ |
/// The alternative, dialling the control server here, means a network call on a
|
|
239 |
+ |
/// keypress and a second timeout to explain.
|
|
240 |
+ |
///
|
|
241 |
+ |
/// The mesh's own interfaces are skipped. `tailscale0` carries a default route
|
|
242 |
+ |
/// when an exit node is set, and counting it would let a machine whose only
|
|
243 |
+ |
/// route is the mesh conclude it can go and join the mesh.
|
|
244 |
+ |
fn has_route(table: &str) -> bool {
|
|
245 |
+ |
table
|
|
246 |
+ |
.lines()
|
|
247 |
+ |
.skip(1)
|
|
248 |
+ |
.filter_map(|line| {
|
|
249 |
+ |
let mut cols = line.split_whitespace();
|
|
250 |
+ |
let iface = cols.next()?;
|
|
251 |
+ |
let destination = cols.next()?;
|
|
252 |
+ |
Some((iface, destination))
|
|
253 |
+ |
})
|
|
254 |
+ |
.any(|(iface, destination)| destination == "00000000" && !iface.starts_with("tailscale"))
|
|
255 |
+ |
}
|
|
256 |
+ |
|
|
257 |
+ |
/// The same question, of this machine.
|
|
258 |
+ |
///
|
|
259 |
+ |
/// A missing `/proc/net/route` answers yes rather than no. This gate exists to
|
|
260 |
+ |
/// catch a known state, and a console that refuses to enroll because it could
|
|
261 |
+ |
/// not read a procfs file would be inventing a second failure to explain the
|
|
262 |
+ |
/// first.
|
|
263 |
+ |
pub(crate) fn machine_has_route() -> bool {
|
|
264 |
+ |
match std::fs::read_to_string("/proc/net/route") {
|
|
265 |
+ |
Ok(table) => has_route(&table),
|
|
266 |
+ |
Err(_) => true,
|
|
267 |
+ |
}
|
|
268 |
+ |
}
|
|
269 |
+ |
|
|
270 |
+ |
/// What to say when there is no route, naming the screen that fixes it.
|
|
271 |
+ |
pub(crate) const NO_ROUTE: &str = "no network: signing in needs a route to the control server. Join one in `alloy net`, then enroll.";
|
|
272 |
+ |
|
| 224 |
273 |
|
pub(crate) fn validate_login_server(value: &str) -> Result<Option<String>, String> {
|
| 225 |
274 |
|
let value = value.trim();
|
| 226 |
275 |
|
if value.is_empty() {
|
| 707 |
756 |
|
return Flow::Continue;
|
| 708 |
757 |
|
}
|
| 709 |
758 |
|
};
|
|
759 |
+ |
if !machine_has_route() {
|
|
760 |
+ |
self.error = Some(NO_ROUTE.into());
|
|
761 |
+ |
self.close_enrollment();
|
|
762 |
+ |
return Flow::Continue;
|
|
763 |
+ |
}
|
| 710 |
764 |
|
let invocation = self.backend.enroll(server.as_deref());
|
| 711 |
765 |
|
self.close_enrollment();
|
| 712 |
766 |
|
// Recorded before the handover, as in `alloy pkg`: the pane carries what
|