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:
@@ -8,6 +8,10 @@ pub struct RegisterRequest {
|
||||
pub version: u16,
|
||||
pub node_key: String,
|
||||
pub old_node_key: String,
|
||||
/// Curve25519 disco public key. Headscale persists this on the node
|
||||
/// record and uses it for peer-to-peer discovery — if it's zero, peers
|
||||
/// won't include us in their netmaps.
|
||||
pub disco_key: String,
|
||||
pub auth: Option<AuthInfo>,
|
||||
pub hostinfo: HostInfo,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -93,6 +97,14 @@ pub struct MapRequest {
|
||||
pub node_key: String,
|
||||
pub disco_key: String,
|
||||
pub stream: bool,
|
||||
/// "Lite update" flag — set together with `Stream: false` and
|
||||
/// `ReadOnly: false` to make Headscale persist DiscoKey + endpoints
|
||||
/// without sending a full netmap response (the "Lite endpoint update"
|
||||
/// path in Headscale's poll.go).
|
||||
#[serde(default)]
|
||||
pub omit_peers: bool,
|
||||
#[serde(default)]
|
||||
pub read_only: bool,
|
||||
pub hostinfo: HostInfo,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub endpoints: Option<Vec<String>>,
|
||||
@@ -237,6 +249,7 @@ mod tests {
|
||||
version: 74,
|
||||
node_key: "nodekey:aabb".into(),
|
||||
old_node_key: "".into(),
|
||||
disco_key: "discokey:ccdd".into(),
|
||||
auth: Some(AuthInfo {
|
||||
auth_key: Some("tskey-abc".into()),
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user