feat(net): scaffold sunbeam-net crate with foundations

Add the workspace crate that will host a pure Rust Headscale/Tailscale-
compatible VPN client. This first commit lands the crate skeleton plus
the leaf modules that the rest of the stack builds on:

- error: thiserror Error enum + Result alias
- config: VpnConfig
- keys: Curve25519 node/disco/wg key types with on-disk persistence
- proto/types: PascalCase serde wire types matching Tailscale's JSON
This commit is contained in:
2026-04-07 13:40:27 +01:00
parent cc2c3f7a3b
commit 13539e6e85
9 changed files with 1130 additions and 30 deletions

80
sunbeam-net/src/error.rs Normal file
View File

@@ -0,0 +1,80 @@
/// Errors produced by sunbeam-net.
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("noise handshake failed: {0}")]
Noise(String),
#[error("control protocol error: {0}")]
Control(String),
#[error("wireguard error: {0}")]
WireGuard(String),
#[error("DERP relay error: {0}")]
Derp(String),
#[error("authentication failed: {0}")]
Auth(String),
#[error("daemon error: {0}")]
Daemon(String),
#[error("IPC error: {0}")]
Ipc(String),
#[error("{context}: {source}")]
Io {
context: String,
#[source]
source: std::io::Error,
},
#[error("{0}")]
Json(#[from] serde_json::Error),
#[error("connection closed")]
ConnectionClosed,
#[error("{0}")]
Other(String),
}
pub type Result<T> = std::result::Result<T, Error>;
impl From<h2::Error> for Error {
fn from(e: h2::Error) -> Self {
Error::Control(e.to_string())
}
}
/// Extension trait for adding context to `Result` types.
pub trait ResultExt<T> {
fn ctx(self, context: &str) -> Result<T>;
fn with_ctx<F: FnOnce() -> String>(self, f: F) -> Result<T>;
}
impl<T> ResultExt<T> for std::result::Result<T, std::io::Error> {
fn ctx(self, context: &str) -> Result<T> {
self.map_err(|source| Error::Io {
context: context.to_string(),
source,
})
}
fn with_ctx<F: FnOnce() -> String>(self, f: F) -> Result<T> {
self.map_err(|source| Error::Io {
context: f(),
source,
})
}
}
impl<T> ResultExt<T> for Result<T> {
fn ctx(self, context: &str) -> Result<T> {
self.map_err(|e| Error::Other(format!("{context}: {e}")))
}
fn with_ctx<F: FnOnce() -> String>(self, f: F) -> Result<T> {
self.map_err(|e| Error::Other(format!("{}: {e}", f())))
}
}