Make a network on the spot, and say which one you just started
Two reports of the same shape: a network was left and came back after a restart, and `network join` asked for a secret it could have invented. The first was not a bug in leaving. The secret on the start command line derives the network id, so a command line carrying the secret of a network you have just left recreates it on the next start — which is right, it says to join that network, but nothing on screen said so. `up` now marks the network `· new` or `· already here`, and warns in full when another configured network answers to the same name. A name is a label; the id is the identity, and the secret is what decides which of them this is. Said at the moment it happens it is obvious; discovered later in a status report it is a mystery, which is exactly how it went. The second was an omission: `up` had learned to invent a secret and `network join` had not, so the quickest possible thing — a network with somebody for as long as it is needed, then gone — still needed a secret generated first. Both now resolve a bare name the same way: the one network of that name this device already has, or a fresh random secret when there is none. It is printed in full, with the single line the other person can paste as it stands, endpoint id included, because a secret nobody can read is a network nobody can join. The id is printed in full by both answers now. The shortened form belongs in a report, where it is read; this one gets copied into the next command. Covered end to end against a running agent: joining with no secret prints a secret and a pasteable command with a peer in it, and joining a name this device already has resumes that network instead of making another that merely looks the same. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -323,7 +323,7 @@ tsunagi id key rotate replace that key
|
|||||||
|
|
||||||
tsunagi network the networks this device belongs to
|
tsunagi network the networks this device belongs to
|
||||||
tsunagi network join -n lab -s tsn1… join one; adds it to a running agent
|
tsunagi network join -n lab -s tsn1… join one; adds it to a running agent
|
||||||
tsunagi network join -n lab join one whose secret this device already has
|
tsunagi network join -n lab resume one this device has, or make it on the spot
|
||||||
tsunagi network leave <id> give up the address and name, then forget it
|
tsunagi network leave <id> give up the address and name, then forget it
|
||||||
tsunagi network secret the secret of each joined network
|
tsunagi network secret the secret of each joined network
|
||||||
tsunagi network secret <id> just that one, for copying
|
tsunagi network secret <id> just that one, for copying
|
||||||
@@ -334,8 +334,8 @@ tsunagi dns on start it, now and after every restart
|
|||||||
tsunagi dns off stop it, now and after every restart
|
tsunagi dns off stop it, now and after every restart
|
||||||
```
|
```
|
||||||
|
|
||||||
**A network without a secret makes one.** `tsunagi up --network lab` with no
|
**A network without a secret makes one.** `tsunagi up --network lab` and
|
||||||
`--secret` resolves in the obvious way: if this device is already in exactly
|
`tsunagi network join --network lab` both resolve a bare name the same way: if this device is already in exactly
|
||||||
one network called `lab`, that one — so the name alone resumes what you have;
|
one network called `lab`, that one — so the name alone resumes what you have;
|
||||||
if it is in none, a fresh random secret, printed in full along with the one
|
if it is in none, a fresh random secret, printed in full along with the one
|
||||||
line to send the others:
|
line to send the others:
|
||||||
@@ -351,6 +351,13 @@ other machine:
|
|||||||
tsunagi up --network lab --secret tsn1u7c… --peer 91e83a6e2b7a…
|
tsunagi up --network lab --secret tsn1u7c… --peer 91e83a6e2b7a…
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`up` says which of the two happened — `· new` or `· already here` beside
|
||||||
|
the network — and warns when the name is one another configured network
|
||||||
|
also answers to, because a name is a label and the id is the identity. A
|
||||||
|
command line with a different secret makes a *different* network of the
|
||||||
|
same name, and that is how a network you left comes back: the secret on the
|
||||||
|
command line is what decides which network it is.
|
||||||
|
|
||||||
That is the ad-hoc case: one person makes a network and sends the command
|
That is the ad-hoc case: one person makes a network and sends the command
|
||||||
round. The secret is printed *only* when the agent invented it — there is
|
round. The secret is printed *only* when the agent invented it — there is
|
||||||
nowhere else to read it from — and never when it was supplied, because then
|
nowhere else to read it from — and never when it was supplied, because then
|
||||||
|
|||||||
+157
-11
@@ -490,6 +490,30 @@ fn store_dns_setting(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// What this device already knows about the network a command names.
|
||||||
|
///
|
||||||
|
/// Two things, and both are only knowable *before* joining: whether this
|
||||||
|
/// network was already configured here, and whether another one answers to
|
||||||
|
/// the same name. A name is a label and an id is the identity, so those two
|
||||||
|
/// are different networks that share nothing — almost always a mistyped
|
||||||
|
/// secret, and the one mistake that makes a report unreadable. Said at the
|
||||||
|
/// moment it happens, it is obvious; discovered later in a status report,
|
||||||
|
/// it is a mystery.
|
||||||
|
fn network_context(
|
||||||
|
configured: &[tsunagi::storage::StoredNetwork],
|
||||||
|
name: &NetworkName,
|
||||||
|
network_id: tsunagi::NetworkId,
|
||||||
|
) -> (bool, Option<String>) {
|
||||||
|
let known = configured
|
||||||
|
.iter()
|
||||||
|
.any(|other| other.network_id == network_id);
|
||||||
|
let shared = configured
|
||||||
|
.iter()
|
||||||
|
.find(|other| other.name == *name && other.network_id != network_id)
|
||||||
|
.map(|other| other.network_id.to_string());
|
||||||
|
(known, shared)
|
||||||
|
}
|
||||||
|
|
||||||
/// Where the secret a command is about to use came from.
|
/// Where the secret a command is about to use came from.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
enum SecretOrigin {
|
enum SecretOrigin {
|
||||||
@@ -1361,8 +1385,13 @@ async fn network_command(args: NetworkArgs) -> Result<(), Box<dyn std::error::Er
|
|||||||
secret,
|
secret,
|
||||||
secret_file,
|
secret_file,
|
||||||
}) => {
|
}) => {
|
||||||
let secret = load_secret(secret.as_deref(), secret_file.as_deref())?;
|
let name = NetworkName::new(network)?;
|
||||||
join_network(&paths, &socket, &network, secret).await
|
// The same rule as `up`: a name this device already has means
|
||||||
|
// that network, a name nobody has means a new one, and no
|
||||||
|
// secret is needed to make a network with a friend in a hurry.
|
||||||
|
let (secret, origin) =
|
||||||
|
resolve_secret(&paths, &name, secret.as_deref(), secret_file.as_deref())?;
|
||||||
|
join_network(&paths, &socket, &name, secret, origin).await
|
||||||
}
|
}
|
||||||
Some(NetworkAction::Leave { network, offline }) => {
|
Some(NetworkAction::Leave { network, offline }) => {
|
||||||
leave_network(&paths, &socket, &network, offline).await
|
leave_network(&paths, &socket, &network, offline).await
|
||||||
@@ -1390,20 +1419,23 @@ async fn network_command(args: NetworkArgs) -> Result<(), Box<dyn std::error::Er
|
|||||||
async fn join_network(
|
async fn join_network(
|
||||||
paths: &StoragePaths,
|
paths: &StoragePaths,
|
||||||
socket: &std::path::Path,
|
socket: &std::path::Path,
|
||||||
name: &str,
|
name: &NetworkName,
|
||||||
secret: NetworkSecret,
|
secret: NetworkSecret,
|
||||||
|
origin: SecretOrigin,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
// The running agent, because a second `up` cannot have the directory
|
// The running agent, because a second `up` cannot have the directory
|
||||||
// and because this way the network starts at once instead of at the
|
// and because this way the network starts at once instead of at the
|
||||||
// next restart.
|
// next restart.
|
||||||
if socket.exists() {
|
if socket.exists() {
|
||||||
let report =
|
let report =
|
||||||
tsunagi::ipc::unix::join_network(socket, name, secret.encode().as_str()).await?;
|
tsunagi::ipc::unix::join_network(socket, name.as_str(), secret.encode().as_str())
|
||||||
|
.await?;
|
||||||
|
// The id in full either way: it is what every other command takes,
|
||||||
|
// and the shortened form in a report is for reading, not copying.
|
||||||
if report.already_configured {
|
if report.already_configured {
|
||||||
println!(
|
println!(
|
||||||
"`{}` ({}) was already configured; it is running",
|
"`{}` ({}) was already configured; it is running",
|
||||||
report.name,
|
report.name, report.network_id
|
||||||
short(&report.network_id, 10)
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
println!("joined `{}` ({})", report.name, report.network_id);
|
println!("joined `{}` ({})", report.name, report.network_id);
|
||||||
@@ -1417,13 +1449,15 @@ async fn join_network(
|
|||||||
short(other, 10)
|
short(other, 10)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if origin == SecretOrigin::Generated {
|
||||||
|
invite(socket, name, &secret).await;
|
||||||
|
}
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
// No agent: configure it, and say when it will take effect rather than
|
// No agent: configure it, and say when it will take effect rather than
|
||||||
// leaving the impression that it is running.
|
// leaving the impression that it is running.
|
||||||
let name = NetworkName::new(name)?;
|
let keys = tsunagi::identity::NetworkKeys::derive(name, &secret);
|
||||||
let keys = tsunagi::identity::NetworkKeys::derive(&name, &secret);
|
|
||||||
let storage = tsunagi::storage::Storage::open(paths)?;
|
let storage = tsunagi::storage::Storage::open(paths)?;
|
||||||
let existing = storage.list_networks().await.unwrap_or_default();
|
let existing = storage.list_networks().await.unwrap_or_default();
|
||||||
let already = existing
|
let already = existing
|
||||||
@@ -1431,10 +1465,10 @@ async fn join_network(
|
|||||||
.any(|other| other.network_id == keys.network_id());
|
.any(|other| other.network_id == keys.network_id());
|
||||||
let shared = existing
|
let shared = existing
|
||||||
.iter()
|
.iter()
|
||||||
.find(|other| other.name == name && other.network_id != keys.network_id())
|
.find(|other| other.name == *name && other.network_id != keys.network_id())
|
||||||
.map(|other| other.network_id.to_string());
|
.map(|other| other.network_id.to_string());
|
||||||
storage
|
storage
|
||||||
.upsert_network(keys.network_id(), name.clone(), secret, true)
|
.upsert_network(keys.network_id(), name.clone(), secret.clone(), true)
|
||||||
.await?;
|
.await?;
|
||||||
storage.release_ownership_lock();
|
storage.release_ownership_lock();
|
||||||
|
|
||||||
@@ -1450,10 +1484,42 @@ async fn join_network(
|
|||||||
short(&other, 10)
|
short(&other, 10)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if origin == SecretOrigin::Generated {
|
||||||
|
println!(" secret {}", secret.encode().as_str());
|
||||||
|
}
|
||||||
eprintln!("\nNo agent is running here, so it starts with the next `tsunagi up`.");
|
eprintln!("\nNo agent is running here, so it starts with the next `tsunagi up`.");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Prints the one line that gets somebody else into this network.
|
||||||
|
///
|
||||||
|
/// Only when the secret was invented here: there is nowhere else to read it
|
||||||
|
/// from, and the whole point of a network made in a hurry is that the
|
||||||
|
/// command can be pasted to the other person as it stands. The endpoint id
|
||||||
|
/// comes from the running agent, because without a peer to contact the
|
||||||
|
/// other side has nothing to go on.
|
||||||
|
async fn invite(socket: &std::path::Path, name: &NetworkName, secret: &NetworkSecret) {
|
||||||
|
let endpoint = tsunagi::ipc::unix::request_status(socket)
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.map(|report| report.endpoint_id)
|
||||||
|
.filter(|id| !id.is_empty());
|
||||||
|
println!(" secret {}", secret.encode().as_str());
|
||||||
|
match endpoint {
|
||||||
|
Some(endpoint) => println!(
|
||||||
|
"\nRun this on the other machine:\n\n \
|
||||||
|
tsunagi up --network {name} --secret {} --peer {endpoint}",
|
||||||
|
secret.encode().as_str()
|
||||||
|
),
|
||||||
|
None => println!(
|
||||||
|
"\nRun this on the other machine, with this device's endpoint id from \
|
||||||
|
`tsunagi id`:\n\n \
|
||||||
|
tsunagi up --network {name} --secret {} --peer <endpoint-id>",
|
||||||
|
secret.encode().as_str()
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Every configured network, live where an agent can say so.
|
/// Every configured network, live where an agent can say so.
|
||||||
async fn show_networks(
|
async fn show_networks(
|
||||||
paths: &StoragePaths,
|
paths: &StoragePaths,
|
||||||
@@ -2924,6 +2990,11 @@ async fn up(args: UpArgs) -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
args.secret.as_deref(),
|
args.secret.as_deref(),
|
||||||
args.secret_file.as_deref(),
|
args.secret_file.as_deref(),
|
||||||
)?;
|
)?;
|
||||||
|
// Read before anything joins, because afterwards everything is
|
||||||
|
// configured and the difference is what the user needs to see.
|
||||||
|
let network_id = tsunagi::identity::NetworkKeys::derive(&name, &secret).network_id();
|
||||||
|
let (known_before, name_shared_with) =
|
||||||
|
network_context(&stored_networks(&paths), &name, network_id);
|
||||||
|
|
||||||
// Parsed up front so a typo is reported immediately, and so the option is
|
// Parsed up front so a typo is reported immediately, and so the option is
|
||||||
// never silently ignored when the data plane is off.
|
// never silently ignored when the data plane is off.
|
||||||
@@ -3081,7 +3152,14 @@ async fn up(args: UpArgs) -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
println!("tsunagi is up");
|
println!("tsunagi is up");
|
||||||
println!(" endpoint id {}", agent.endpoint_id());
|
println!(" endpoint id {}", agent.endpoint_id());
|
||||||
println!(" hostname {}", agent.hostname());
|
println!(" hostname {}", agent.hostname());
|
||||||
println!(" network {name} ({network})");
|
// Whether this command line just made a network or picked up one that
|
||||||
|
// was already here. Without it, a secret that has quietly created a
|
||||||
|
// second network of the same name — or recreated one that was left —
|
||||||
|
// looks exactly like the network you meant.
|
||||||
|
println!(
|
||||||
|
" network {name} ({network}) · {}",
|
||||||
|
if known_before { "already here" } else { "new" }
|
||||||
|
);
|
||||||
println!(" state {}", paths.state_dir.display());
|
println!(" state {}", paths.state_dir.display());
|
||||||
// One line, everything the other side needs, ready to paste. The
|
// One line, everything the other side needs, ready to paste. The
|
||||||
// secret is printed in full only when this agent invented it: then
|
// secret is printed in full only when this agent invented it: then
|
||||||
@@ -3130,6 +3208,19 @@ async fn up(args: UpArgs) -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// A name is a label and an id is the identity, so two networks can be
|
||||||
|
// called the same thing and share nothing. Almost always a mistyped
|
||||||
|
// secret, and the one mistake that makes a report unreadable.
|
||||||
|
if let Some(other) = &name_shared_with {
|
||||||
|
eprintln!(
|
||||||
|
"\nwarning: `{name}` is also configured here with a different secret, as {}.\n\
|
||||||
|
A network is its name *and* its secret, so these two share nothing. If that\n\
|
||||||
|
was not meant, `tsunagi network leave` removes one — and check the secret on\n\
|
||||||
|
this command line, because it is what decides which network this is.",
|
||||||
|
short(other, 10)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Last, after the facts, because it is the line to act on: one
|
// Last, after the facts, because it is the line to act on: one
|
||||||
// command with everything the other side needs.
|
// command with everything the other side needs.
|
||||||
if args.peers.is_empty() {
|
if args.peers.is_empty() {
|
||||||
@@ -3959,3 +4050,58 @@ mod secret_tests {
|
|||||||
assert!(err.to_string().contains("does not say which"), "{err}");
|
assert!(err.to_string().contains("does not say which"), "{err}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod network_context_tests {
|
||||||
|
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use tsunagi::identity::NetworkKeys;
|
||||||
|
use tsunagi::storage::StoredNetwork;
|
||||||
|
|
||||||
|
fn configured(name: &str, seed: u8) -> StoredNetwork {
|
||||||
|
let name = NetworkName::new(name).unwrap();
|
||||||
|
let secret = NetworkSecret::from_bytes([seed; 32]).unwrap();
|
||||||
|
let keys = NetworkKeys::derive(&name, &secret);
|
||||||
|
StoredNetwork {
|
||||||
|
network_id: keys.network_id(),
|
||||||
|
name,
|
||||||
|
secret,
|
||||||
|
auto_start: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_network_already_here_is_told_from_a_new_one() {
|
||||||
|
// `up` prints which of the two happened. Without it, a command line
|
||||||
|
// that quietly recreates a network somebody just left looks exactly
|
||||||
|
// like the one they meant to start.
|
||||||
|
let known = configured("lab", 1);
|
||||||
|
let name = known.name.clone();
|
||||||
|
let (already, shared) =
|
||||||
|
network_context(std::slice::from_ref(&known), &name, known.network_id);
|
||||||
|
assert!(already);
|
||||||
|
assert_eq!(shared, None);
|
||||||
|
|
||||||
|
let fresh = configured("lab", 2);
|
||||||
|
let (already, shared) =
|
||||||
|
network_context(std::slice::from_ref(&known), &name, fresh.network_id);
|
||||||
|
assert!(!already, "a different secret is a different network");
|
||||||
|
assert_eq!(
|
||||||
|
shared,
|
||||||
|
Some(known.network_id.to_string()),
|
||||||
|
"and the one it shares a name with is named"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_name_nobody_here_uses_shares_with_nothing() {
|
||||||
|
let (already, shared) = network_context(
|
||||||
|
&[configured("lab", 1)],
|
||||||
|
&NetworkName::new("other").unwrap(),
|
||||||
|
configured("other", 3).network_id,
|
||||||
|
);
|
||||||
|
assert!(!already);
|
||||||
|
assert_eq!(shared, None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
//! Making and joining a network from the command line, against a real agent.
|
||||||
|
//!
|
||||||
|
//! The rules here are small and easy to get wrong in a way no unit test
|
||||||
|
//! notices: what a bare network name means, when a secret is invented, and
|
||||||
|
//! whether what is printed is enough for the other person to paste.
|
||||||
|
|
||||||
|
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||||
|
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use tempfile::TempDir;
|
||||||
|
|
||||||
|
/// An agent running as a real process, with its own directories.
|
||||||
|
struct Running {
|
||||||
|
child: std::process::Child,
|
||||||
|
dir: TempDir,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Running {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let _ = self.child.kill();
|
||||||
|
let _ = self.child.wait();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Running {
|
||||||
|
fn run(&self, args: &[&str]) -> std::process::Output {
|
||||||
|
std::process::Command::new(env!("CARGO_BIN_EXE_tsunagi"))
|
||||||
|
.args(args)
|
||||||
|
.arg("--state-dir")
|
||||||
|
.arg(self.dir.path().join("state"))
|
||||||
|
.arg("--cache-dir")
|
||||||
|
.arg(self.dir.path().join("cache"))
|
||||||
|
.output()
|
||||||
|
.expect("the agent binary runs")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start(network: &str, port: u16) -> Running {
|
||||||
|
let dir = TempDir::new().unwrap();
|
||||||
|
let child = std::process::Command::new(env!("CARGO_BIN_EXE_tsunagi"))
|
||||||
|
.args([
|
||||||
|
"up",
|
||||||
|
"--network",
|
||||||
|
network,
|
||||||
|
"--secret",
|
||||||
|
"a-secret-for-the-cli-test",
|
||||||
|
])
|
||||||
|
.arg("--state-dir")
|
||||||
|
.arg(dir.path().join("state"))
|
||||||
|
.arg("--cache-dir")
|
||||||
|
.arg(dir.path().join("cache"))
|
||||||
|
.args(["--reach", "local", "--no-tun"])
|
||||||
|
.arg("--bind")
|
||||||
|
.arg(format!("127.0.0.1:{port}"))
|
||||||
|
.args(["--log", "error", "--status-interval", "0"])
|
||||||
|
.stdout(std::process::Stdio::null())
|
||||||
|
.stderr(std::process::Stdio::null())
|
||||||
|
.spawn()
|
||||||
|
.expect("the agent binary starts");
|
||||||
|
// Long enough for the control socket to be there to talk to.
|
||||||
|
std::thread::sleep(Duration::from_secs(2));
|
||||||
|
Running { child, dir }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn joining_with_no_secret_makes_one_and_prints_what_to_send() {
|
||||||
|
// The hurry case: a network with somebody, for as long as it is needed,
|
||||||
|
// and then gone. Asking for a secret first is a step with no purpose,
|
||||||
|
// and a secret that is not printed is a network nobody else can join.
|
||||||
|
let agent = start("resident", 45071);
|
||||||
|
|
||||||
|
let joined = agent.run(&["network", "join", "--network", "spontaneous"]);
|
||||||
|
assert!(
|
||||||
|
joined.status.success(),
|
||||||
|
"joining failed: {}",
|
||||||
|
String::from_utf8_lossy(&joined.stderr)
|
||||||
|
);
|
||||||
|
let out = String::from_utf8_lossy(&joined.stdout);
|
||||||
|
assert!(out.contains("joined `spontaneous`"), "{out}");
|
||||||
|
|
||||||
|
// The secret in full, and one line the other side can paste as it is.
|
||||||
|
let secret = out
|
||||||
|
.lines()
|
||||||
|
.find_map(|line| line.trim().strip_prefix("secret "))
|
||||||
|
.expect("the invented secret is printed");
|
||||||
|
assert!(secret.starts_with("tsn1"), "{out}");
|
||||||
|
let command = out
|
||||||
|
.lines()
|
||||||
|
.find(|line| line.contains("tsunagi up --network spontaneous"))
|
||||||
|
.expect("a command to send");
|
||||||
|
assert!(command.contains(secret), "with the secret in it: {out}");
|
||||||
|
assert!(
|
||||||
|
command.contains("--peer "),
|
||||||
|
"and somewhere to find us: {out}"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Joining the same name again resumes it rather than making another
|
||||||
|
// network that merely looks the same.
|
||||||
|
let again = agent.run(&["network", "join", "--network", "spontaneous"]);
|
||||||
|
let out = String::from_utf8_lossy(&again.stdout);
|
||||||
|
assert!(again.status.success());
|
||||||
|
assert!(out.contains("already configured"), "{out}");
|
||||||
|
assert!(!out.contains("secret tsn1"), "no second secret: {out}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_bare_name_resumes_the_network_of_that_name_rather_than_inventing_one() {
|
||||||
|
// `up --network resident` with no secret is the same rule: this device
|
||||||
|
// has exactly one network of that name, so that is the one meant.
|
||||||
|
let agent = start("resident", 45072);
|
||||||
|
let listed = agent.run(&["network"]);
|
||||||
|
let before = String::from_utf8_lossy(&listed.stdout).to_string();
|
||||||
|
let id = before
|
||||||
|
.lines()
|
||||||
|
.find(|line| line.contains("resident"))
|
||||||
|
.and_then(|line| line.split_whitespace().nth(1))
|
||||||
|
.expect("the network is listed")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let joined = agent.run(&["network", "join", "--network", "resident"]);
|
||||||
|
assert!(joined.status.success());
|
||||||
|
let out = String::from_utf8_lossy(&joined.stdout);
|
||||||
|
assert!(out.contains(&id), "the same network, not a new one: {out}");
|
||||||
|
assert!(out.contains("already configured"), "{out}");
|
||||||
|
}
|
||||||
@@ -69,6 +69,11 @@ asking a running agent for status over a real Unix socket, joining and
|
|||||||
leaving a network through it, a leftover socket file being replaced while a
|
leaving a network through it, a leftover socket file being replaced while a
|
||||||
live one is not, and the derived socket path staying short enough to bind.
|
live one is not, and the derived socket path staying short enough to bind.
|
||||||
|
|
||||||
|
`crates/tsunagi-cli/tests/network_cli.rs` runs the real binary too: joining
|
||||||
|
with no secret invents one, prints it in full and prints a line the other
|
||||||
|
side can paste unchanged, while a bare name this device already knows
|
||||||
|
resumes that network instead of inventing another of the same name.
|
||||||
|
|
||||||
`crates/tsunagi-cli/tests/dns_service.rs` runs the real binary: the resolver
|
`crates/tsunagi-cli/tests/dns_service.rs` runs the real binary: the resolver
|
||||||
comes up with no interface to attach it to, the listener is not rebuilt on
|
comes up with no interface to attach it to, the listener is not rebuilt on
|
||||||
the way past, a name outside every zone is refused, each network gets a zone
|
the way past, a name outside every zone is refused, each network gets a zone
|
||||||
|
|||||||
Reference in New Issue
Block a user