Serve a zone per network, and make a network in one command
Twice now a report has read "dns not serving" and been taken for a broken resolver. It was accurate both times: the agent had been started without `--dns`. That is the flag's fault, not the reader's — a resolver that disappears because one word was not retyped is worse than none, since the names simply stop working. So the setting belongs to the device now: `--dns` turns it on and it stays on, `tsunagi dns off` turns it off, and `tsunagi dns on` turns it on for an agent that is already running, without restarting it. `tsunagi dns` says what it is doing, or what it will do at the next start when nothing is running. One agent has one identity and as many networks as it likes, so one DNS service serves them all: each network is a zone named after it, and joining or leaving one changes what resolves with no restart. A question carries a name and not the network it belongs to, so the suffix decides and nothing is shared between zones — a member of one network is not a name in another. `--dns-zone` is gone with that: there is no single zone to name any more, and a network name may contain dots, so `--network lab.internal` is how you get `music.lab.internal`. It listens on loopback only, where it always could have. Binding the overlay address put the zones in front of the whole mesh, and with several networks on one agent that would have answered one network's questions about another's names. That made a gap plain: a second network on an agent had no addresses at all, because the configured range belongs to whichever network took it first, so its members had nothing to allocate from and no names to answer with. A second network now uses the range **derived from its own network id** — every member derives the same one from something they all already have, so it is an agreement rather than a local invention. It is held back for a moment first, because a network that already exists has a range of its own and a joiner should adopt it rather than argue; that wait is what keeps "the first member settles it" true. And a network needs no ceremony to start. `tsunagi up --network lab` with no secret resolves the obvious way: the one network of that name this device already has, or — when there is none — a fresh random secret, printed in full with the single line to send the others. That is the ad-hoc case, one person makes a network and passes the command round, and it was previously two steps with a flag people could not find. The secret is printed only when the agent invented it, because then there is nowhere else to read it from; one that was supplied is not echoed. Two networks of one name and no secret is the one case with no answer, and it says so rather than choosing. Releasing now also stops this agent claiming again. The periodic check would otherwise publish a fresh claim in the moment between the goodbye and the teardown, turning a release into a hello nobody asked for. Exercised with the real binary: a zone per network as a second one is joined into a running agent, the resolver switched on and off while it runs, and an ad-hoc network printing its secret and the line to share. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -13,7 +13,7 @@ use tsunagi::proto::message::{
|
||||
};
|
||||
use tsunagi::proto::{read_frame, write_frame};
|
||||
use tsunagi::test_support;
|
||||
use tsunagi::testing::{TestAgent, network, settle, wait_event, wait_for_peers};
|
||||
use tsunagi::testing::{TestAgent, network, settle, wait_event, wait_for_peers, wait_until};
|
||||
|
||||
const LIMIT: usize = 64 * 1024;
|
||||
|
||||
@@ -262,3 +262,73 @@ async fn deactivating_one_network_leaves_the_agent_and_others_running() {
|
||||
|
||||
agent.agent.shutdown().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn two_networks_on_one_agent_each_get_a_range_of_their_own() {
|
||||
// One agent, one interface: the configured range can only belong to one
|
||||
// of them. The second does not go without — it takes the range derived
|
||||
// from its own id, which every one of its members derives identically,
|
||||
// so it is an agreement and not a local invention.
|
||||
let discovery = SharedMemoryDiscovery::new();
|
||||
let (first, first_secret) = network("two-ranges-one");
|
||||
let (second, second_secret) = network("two-ranges-two");
|
||||
|
||||
let agent = TestAgent::spawn(&discovery).await.unwrap();
|
||||
let one = agent
|
||||
.agent
|
||||
.join_network(&first, &first_secret)
|
||||
.await
|
||||
.unwrap();
|
||||
let two = agent
|
||||
.agent
|
||||
.join_network(&second, &second_secret)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let own = agent.agent.endpoint_id();
|
||||
let addresses = wait_until("both networks allocate an address", || {
|
||||
let handle = agent.agent.clone();
|
||||
async move {
|
||||
let mine = async |network| {
|
||||
handle
|
||||
.network_status(network)
|
||||
.await
|
||||
.ok()?
|
||||
.members
|
||||
.into_iter()
|
||||
.find(|member| member.endpoint_id == own)?
|
||||
.overlay_address_v4
|
||||
};
|
||||
Some((mine(one).await?, mine(two).await?))
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
assert_ne!(
|
||||
addresses.0, addresses.1,
|
||||
"different networks, different addresses"
|
||||
);
|
||||
assert!(
|
||||
tsunagi::state::DEFAULT_IPV4_RANGE.contains(addresses.0),
|
||||
"the first keeps the configured range: {}",
|
||||
addresses.0
|
||||
);
|
||||
assert!(
|
||||
tsunagi::state::derived_ipv4_range(two).contains(addresses.1),
|
||||
"the second is in the range derived from its id: {}",
|
||||
addresses.1
|
||||
);
|
||||
assert!(
|
||||
!tsunagi::state::DEFAULT_IPV4_RANGE.contains(addresses.1),
|
||||
"and not in the one the first network holds"
|
||||
);
|
||||
|
||||
// And the ranges do not overlap, which is what lets one interface
|
||||
// carry both without a packet being ambiguous.
|
||||
assert!(
|
||||
!tsunagi::state::derived_ipv4_range(two).contains(addresses.0),
|
||||
"the two ranges must not overlap"
|
||||
);
|
||||
|
||||
agent.agent.shutdown().await;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user