fix(net): protocol fixes for Tailscale-compatible peer reachability
A pile of correctness bugs that all stopped real Tailscale peers from being able to send WireGuard packets back to us. Found while building out the e2e test against the docker-compose stack. 1. WireGuard static key was wrong (lifecycle.rs) We were initializing the WgTunnel with `keys.wg_private`, a separate x25519 key from the one Tailscale advertises in netmaps. Peers know us by `node_public` and compute mac1 against it; signing handshakes with a different private key meant every init we sent was silently dropped. Use `keys.node_private` instead — node_key IS the WG static key in Tailscale. 2. DERP relay couldn't route packets to us (derp/client.rs) Our DerpClient was sealing the ClientInfo frame with a fresh ephemeral NaCl keypair and putting the ephemeral public in the frame prefix. Tailscale's protocol expects the *long-term* node public key in the prefix — that's how the relay knows where to forward packets addressed to our node_key. With the ephemeral key, the relay accepted the connection but never delivered our peers' responses. Now seal with the long-term node key. 3. Headscale never persisted our DiscoKey (proto/types.rs, control/*) The streaming /machine/map handler in Headscale ≥ capVer 68 doesn't update DiscoKey on the node record — only the "Lite endpoint update" path does, gated on Stream:false + OmitPeers:true + ReadOnly:false. Without DiscoKey our nodes appeared in `headscale nodes list` with `discokey:000…` and never propagated into peer netmaps. Add the DiscoKey field to RegisterRequest, add OmitPeers/ReadOnly fields to MapRequest, and call a new `lite_update` between register and the streaming map. Also add `post_json_no_response` for endpoints that reply with an empty body. 4. EncapAction is now a struct instead of an enum (wg/tunnel.rs) Routing was forced to either UDP or DERP. With a peer whose advertised UDP endpoint is on an unreachable RFC1918 network (e.g. docker bridge IPs), we'd send via UDP, get nothing, and never fall back. Send over every available transport — receivers dedupe via the WireGuard replay window — and let dispatch_encap forward each populated arm to its respective channel. 5. Drop the dead PacketRouter (wg/router.rs) Skeleton from an earlier design that never got wired up; it's been accumulating dead-code warnings.
This commit is contained in:
@@ -33,7 +33,7 @@ impl DerpClient {
|
||||
/// `url` should be like `http://host:port` or `host:port`.
|
||||
pub async fn connect(
|
||||
url: &str,
|
||||
_node_key: &crate::keys::NodeKeys,
|
||||
node_keys: &crate::keys::NodeKeys,
|
||||
) -> crate::Result<Self> {
|
||||
use crypto_box::aead::{Aead, AeadCore, OsRng};
|
||||
use crypto_box::{PublicKey, SalsaBox, SecretKey};
|
||||
@@ -128,26 +128,32 @@ impl DerpClient {
|
||||
let mut server_public = [0u8; 32];
|
||||
server_public.copy_from_slice(key_bytes);
|
||||
|
||||
// Generate ephemeral NaCl keypair for the handshake
|
||||
let ephemeral_secret = SecretKey::generate(&mut OsRng);
|
||||
let ephemeral_public = ephemeral_secret.public_key();
|
||||
// The DERP relay needs to know our LONG-TERM node public key so it
|
||||
// can route inbound packets addressed to us. Tailscale's protocol
|
||||
// sends the node public key as the first 32 bytes of the ClientInfo
|
||||
// frame, then a NaCl-sealed JSON blob signed with the node's
|
||||
// long-term private key (so the server can verify ownership).
|
||||
let node_secret_bytes: [u8; 32] = node_keys.node_private.to_bytes();
|
||||
let node_public_bytes: [u8; 32] = *node_keys.node_public.as_bytes();
|
||||
let node_secret = SecretKey::from(node_secret_bytes);
|
||||
let node_public_nacl = PublicKey::from(node_public_bytes);
|
||||
|
||||
// Build client info JSON
|
||||
let client_info = serde_json::json!({"version": 2});
|
||||
let client_info_bytes = serde_json::to_vec(&client_info)
|
||||
.map_err(|e| Error::Derp(format!("failed to serialize client info: {e}")))?;
|
||||
|
||||
// Seal with crypto_box: encrypt client info using our ephemeral key and server's public key
|
||||
// Seal with crypto_box: encrypt with OUR long-term private + server public.
|
||||
let server_pk = PublicKey::from(server_public);
|
||||
let salsa_box = SalsaBox::new(&server_pk, &ephemeral_secret);
|
||||
let salsa_box = SalsaBox::new(&server_pk, &node_secret);
|
||||
let nonce = SalsaBox::generate_nonce(&mut OsRng);
|
||||
let sealed = salsa_box
|
||||
.encrypt(&nonce, client_info_bytes.as_slice())
|
||||
.map_err(|e| Error::Derp(format!("failed to seal client info: {e}")))?;
|
||||
|
||||
// ClientInfo frame: 32-byte ephemeral public key + nonce + sealed box
|
||||
// ClientInfo frame: 32-byte long-term public key + nonce + sealed box
|
||||
let mut client_info_payload = BytesMut::new();
|
||||
client_info_payload.extend_from_slice(ephemeral_public.as_bytes());
|
||||
client_info_payload.extend_from_slice(node_public_nacl.as_bytes());
|
||||
client_info_payload.extend_from_slice(&nonce);
|
||||
client_info_payload.extend_from_slice(&sealed);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user