Peers communicating on uncensored networks can benefit from raw WireGuard
performance over direct UDP without QUIC framing or double-encryption
overhead. This provides Tailscale-like direct tunnel performance while
preserving Tsunagi's decentralized mesh architecture.
Key changes:
- Create `tsunagi-wireguard` crate implementing the `IpPlugin` and
`PacketTransport` contracts:
- `WG_PROTOCOL = "wg"` with wire version 1.
- Bounded `WgAnnouncement` carrying WireGuard public keys, listening UDP
ports, and candidate IP addresses (filtering out overlay interfaces).
- `WgKeyStore` managing persistent per-network WireGuard X25519 keys
in SQLite with zeroization on drop.
- `WireguardTransport` providing direct UDP packet transmission, candidate
address tracking, inbound link delivery, and roaming support.
- `WireguardDevice` running userspace WireGuard encryption via `boringtun`,
timer maintenance, and AllowedIPs source enforcement.
- Integrate `wg` into `tsunagi-cli`:
- Set default protocols to `"wg,tcp-tls,wg-quic"` so all protocols are
enabled by default in order of throughput and performance.
- Support `"wireguard"` as a convenience alias for `"wg"` in `--protocol`.
- Bind `PureWgTransport` and launch the inbound datagram accept loop on `up`.
- Pass `pure_wg` plugin to `AgentControl` and update `build_report` so
active WireGuard tunnels and overlay metrics are reported in `tsunagi status`.
- Clean up WireGuard keys on network removal in `forget_protocol_state`.
- Comprehensive test coverage in `tsunagi-wireguard`:
- `announcement_tests`: round-trip serialization and verification of malformed,
mismatched network, zero port, or self-claiming announcements.
- `transport_tests`: loopback UDP link delivery between transports.
- `plugin_tests`: end-to-end two-peer handshake and bidirectional IP packet
transit over direct UDP.
Validation on Linux:
- cargo fmt --all -- --check passed.
- cargo clippy --locked --workspace --all-targets -- -D warnings passed.
- cargo test --locked --workspace --all-targets passed.
161 lines
4.6 KiB
Rust
161 lines
4.6 KiB
Rust
//! WireGuard key material for pure WireGuard transport.
|
|
//!
|
|
//! Keys belong to this protocol plugin alone. They are not derived from
|
|
//! the iroh device key and not derived from the network secret, so compromising
|
|
//! or rotating one does not affect the others.
|
|
//!
|
|
//! Keys are X25519, encoded the way WireGuard encodes them: standard base64
|
|
//! with padding, 44 characters.
|
|
|
|
use boringtun::x25519;
|
|
use data_encoding::BASE64;
|
|
use zeroize::{Zeroize, Zeroizing};
|
|
|
|
use tsunagi::dataplane::PluginError;
|
|
|
|
/// Length of a raw WireGuard key in bytes.
|
|
pub const KEY_LEN: usize = 32;
|
|
|
|
/// Length of the base64 text form of a key.
|
|
pub const KEY_TEXT_LEN: usize = 44;
|
|
|
|
/// Applies the X25519 clamping WireGuard applies to private keys.
|
|
fn clamp(bytes: &mut [u8; KEY_LEN]) {
|
|
bytes[0] &= 248;
|
|
bytes[31] &= 127;
|
|
bytes[31] |= 64;
|
|
}
|
|
|
|
/// A WireGuard public key.
|
|
///
|
|
/// Public, safe to log, and the identity a peer is known by inside the overlay.
|
|
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
|
pub struct WgPublicKey([u8; KEY_LEN]);
|
|
|
|
impl WgPublicKey {
|
|
/// Wraps raw key bytes.
|
|
pub fn from_bytes(bytes: [u8; KEY_LEN]) -> Self {
|
|
Self(bytes)
|
|
}
|
|
|
|
/// The raw key bytes.
|
|
pub fn as_bytes(&self) -> &[u8; KEY_LEN] {
|
|
&self.0
|
|
}
|
|
|
|
/// The key in the form the WireGuard implementation expects.
|
|
pub(crate) fn into_x25519(self) -> x25519::PublicKey {
|
|
x25519::PublicKey::from(self.0)
|
|
}
|
|
|
|
/// Whether this is the all-zero key, which is never a valid peer.
|
|
pub fn is_zero(&self) -> bool {
|
|
self.0 == [0u8; KEY_LEN]
|
|
}
|
|
|
|
/// The base64 form WireGuard uses.
|
|
pub fn encode(&self) -> String {
|
|
BASE64.encode(&self.0)
|
|
}
|
|
|
|
/// Parses the base64 form WireGuard uses.
|
|
pub fn decode(text: &str) -> Result<Self, PluginError> {
|
|
if text.len() != KEY_TEXT_LEN {
|
|
return Err(PluginError::Rejected(format!(
|
|
"a WireGuard key is {KEY_TEXT_LEN} base64 characters, got {}",
|
|
text.len()
|
|
)));
|
|
}
|
|
let raw = BASE64
|
|
.decode(text.as_bytes())
|
|
.map_err(|_| PluginError::Rejected("key is not valid base64".into()))?;
|
|
let bytes = <[u8; KEY_LEN]>::try_from(raw.as_slice())
|
|
.map_err(|_| PluginError::Rejected("key is not 32 bytes".into()))?;
|
|
Ok(Self(bytes))
|
|
}
|
|
|
|
/// A short prefix for logs and diagnostics.
|
|
pub fn fmt_short(&self) -> String {
|
|
self.encode().chars().take(8).collect()
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Display for WgPublicKey {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
f.write_str(&self.encode())
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Debug for WgPublicKey {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
write!(f, "WgPublicKey({})", self.fmt_short())
|
|
}
|
|
}
|
|
|
|
/// A WireGuard private key.
|
|
///
|
|
/// Clamped on creation so it matches what `wg genkey` produces. Zeroized on
|
|
/// drop, and prints as `[redacted]` in Debug so it cannot leak into logs.
|
|
#[derive(Clone)]
|
|
pub struct WgSecretKey {
|
|
bytes: Zeroizing<[u8; KEY_LEN]>,
|
|
public: WgPublicKey,
|
|
}
|
|
|
|
impl WgSecretKey {
|
|
/// Generates a fresh clamped random private key.
|
|
pub fn generate() -> Self {
|
|
let mut bytes = [0u8; KEY_LEN];
|
|
rand::fill(&mut bytes);
|
|
clamp(&mut bytes);
|
|
let secret = x25519::StaticSecret::from(bytes);
|
|
let public = WgPublicKey(*x25519::PublicKey::from(&secret).as_bytes());
|
|
Self {
|
|
bytes: Zeroizing::new(bytes),
|
|
public,
|
|
}
|
|
}
|
|
|
|
/// Wraps existing key bytes, clamping them to a valid X25519 private key.
|
|
pub fn from_bytes(raw: &[u8; KEY_LEN]) -> Self {
|
|
let mut bytes = *raw;
|
|
clamp(&mut bytes);
|
|
let secret = x25519::StaticSecret::from(bytes);
|
|
let public = WgPublicKey(*x25519::PublicKey::from(&secret).as_bytes());
|
|
Self {
|
|
bytes: Zeroizing::new(bytes),
|
|
public,
|
|
}
|
|
}
|
|
|
|
/// The matching public key.
|
|
pub fn public(&self) -> WgPublicKey {
|
|
self.public
|
|
}
|
|
|
|
/// The key in the form the WireGuard implementation expects.
|
|
pub(crate) fn to_static_secret(&self) -> x25519::StaticSecret {
|
|
x25519::StaticSecret::from(*self.bytes)
|
|
}
|
|
|
|
/// The raw key bytes.
|
|
pub fn as_bytes(&self) -> &[u8; KEY_LEN] {
|
|
&self.bytes
|
|
}
|
|
}
|
|
|
|
impl Drop for WgSecretKey {
|
|
fn drop(&mut self) {
|
|
self.bytes.zeroize();
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Debug for WgSecretKey {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
f.debug_struct("WgSecretKey")
|
|
.field("bytes", &"[redacted]")
|
|
.field("public", &self.public)
|
|
.finish()
|
|
}
|
|
}
|