Report members, not symptoms
A peer going away showed up as three unrelated yellow rows: no peers authenticated, N dial failures, one packet to an address nobody owns. Each was true and none of them said the actual thing, which is that a member we know about is offline. Worse, they are cumulative, so after the peer came back the report still looked broken. Two changes behind that. Members are now a list, joined from the three sources that each know part of the answer: signed state says who belongs and keeps saying it while they are away, the session list says who is here, the overlay says whose tunnel is up. Online first, then away, this agent left out because the device section already covers it. An absent member is stated rather than flagged — in a mesh of laptops being away is the ordinary condition — and its failed dials are attributed to it instead of floating free as a network-wide number. Counters are history and no longer grade anything. Grading them is what kept the report red long after the cause had gone. The one exception is context-sensitive rather than cumulative: handshake failures with nobody connected is the signature of a mismatched secret, so that is called out. NetworkStatus grows a member roster from the signed records, and the overlay report carries the endpoint id so a tunnel can be matched to its session. Note what the roster cannot do: a member is in signed state only once it has claimed something, which today means an IPv4 address, so an IPv6-only network still has no durable roster. Hostnames are not persisted either, so an absent member is named by its id. The control socket gained a version word. Adding these fields changed how postcard parses the bytes, and without it a client one build ahead of its agent reported "Found an Option discriminant that wasn't 0 or 1". The check names a mismatch for whichever side is newer; an older agent reading a newer request just drops the connection, so the CLI offers that as a possibility rather than asserting it. It also now separates an agent that is absent, which is an ordinary answer, from one that is there and will not answer, which is a fault. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -157,6 +157,19 @@ pub async fn request_status(path: impl AsRef<Path>) -> Result<StatusReport> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Marks the wire format of the local control socket.
|
||||
///
|
||||
/// `b"TSN"` followed by the version, so a mismatch is recognised as one
|
||||
/// instead of being read as a length. The encoding is postcard, which is not
|
||||
/// self-describing: adding a field to a report changes how the bytes parse,
|
||||
/// and without this a client one build ahead of its agent reports something
|
||||
/// like "Found an Option discriminant that wasn't 0 or 1" — which says
|
||||
/// nothing about the actual problem, that the two are different builds.
|
||||
///
|
||||
/// Bump it whenever [`Request`], [`Response`] or anything they contain
|
||||
/// changes shape.
|
||||
pub const CONTROL_PROTOCOL: u32 = u32::from_be_bytes([b'T', b'S', b'N', 2]);
|
||||
|
||||
async fn write_message<T: serde::Serialize>(stream: &mut UnixStream, value: &T) -> Result<()> {
|
||||
let encoded = postcard::to_stdvec(value)
|
||||
.map_err(|err| Error::Storage(format!("cannot encode a control message: {err}")))?;
|
||||
@@ -164,6 +177,10 @@ async fn write_message<T: serde::Serialize>(stream: &mut UnixStream, value: &T)
|
||||
return Err(Error::Storage("control message is too large".into()));
|
||||
}
|
||||
let len = encoded.len() as u32;
|
||||
stream
|
||||
.write_all(&CONTROL_PROTOCOL.to_be_bytes())
|
||||
.await
|
||||
.map_err(io_error)?;
|
||||
stream
|
||||
.write_all(&len.to_be_bytes())
|
||||
.await
|
||||
@@ -174,6 +191,16 @@ async fn write_message<T: serde::Serialize>(stream: &mut UnixStream, value: &T)
|
||||
|
||||
async fn read_message<T: for<'de> serde::Deserialize<'de>>(stream: &mut UnixStream) -> Result<T> {
|
||||
let mut header = [0u8; 4];
|
||||
stream.read_exact(&mut header).await.map_err(io_error)?;
|
||||
let version = u32::from_be_bytes(header);
|
||||
if version != CONTROL_PROTOCOL {
|
||||
return Err(Error::Storage(format!(
|
||||
"the other end speaks control protocol {version:#010x} and this build speaks \
|
||||
{CONTROL_PROTOCOL:#010x}; they are different builds of tsunagi, so restart the \
|
||||
agent with the binary you are running now"
|
||||
)));
|
||||
}
|
||||
|
||||
stream.read_exact(&mut header).await.map_err(io_error)?;
|
||||
let len = u32::from_be_bytes(header) as usize;
|
||||
// Checked before allocating, exactly as on the network.
|
||||
|
||||
Reference in New Issue
Block a user