Gunnar

nordisk/tunnr-tailscale

tests/netmap_wire.rs

//! **The netmap conversion against captured control-plane bytes.**
//!
//! # What these bytes are, and what they are not
//!
//! `tests/fixtures/map_response.json` and `node.json` are **real documents
//! recorded from Tailscale's coordination server**, vendored from
//! `tailscale/tailscale-rs` at commit
//! `ce8f3d91c56d8bc7739ced07dd6fbe5d98822a10` under BSD-3-Clause — see
//! `THIRD-PARTY.md`, where the attribution was added in the same commit as
//! these files, per this repository's rule.
//!
//! **No test in this crate has spoken to a coordination server.** Joining a
//! tailnet needs an auth key, and this lane holds none for any tailnet. So
//! every test here is named `captured_*`: it proves the conversion agrees with
//! bytes Tailscale's control plane really emitted, and it proves nothing about
//! a live session. That distinction is the reason for the naming convention
//! rather than a bare `it_works`.
//!
//! The one thing that would make these live — an auth key — is also the one
//! thing that would not help much, because [`tunnr_tailscale::Reachability`]
//! already says the resulting peer set cannot carry traffic without DERP.

use tunnr_tailscale::{
    netmap::NodeFindingKind, peers_from_map_response, MissingPiece, TailnetPeerSet,
};
use ts_control_serde::MapResponse;

const MAP_RESPONSE: &str = include_str!("fixtures/map_response.json");

fn parsed() -> TailnetPeerSet {
    let response: MapResponse<'_> =
        serde_json::from_str(MAP_RESPONSE).expect("the captured MapResponse parses");
    peers_from_map_response(&response)
}

/// The whole conversion, over bytes a real coordination server sent.
#[test]
fn captured_map_response_becomes_peers_the_data_plane_understands() {
    let set = parsed();
    assert_eq!(
        set.peers.len(),
        5,
        "the captured response carries five peers in PeersChanged: {:?}",
        set.findings
    );

    for peer in &set.peers {
        // A key of all zeroes is what a missing field deserialises to. It is
        // 32 valid-looking bytes and would build a peer nothing can hand-shake
        // with, so no peer may carry one.
        assert_ne!(peer.public_key, [0u8; 32], "no peer carries an identity key");
        // Every peer routes something, or it could never be selected for an
        // outbound packet.
        assert!(!peer.allowed_ips.is_empty());
        // Every route is in Tailscale's own 100.64.0.0/10 space.
        for (net, prefix) in &peer.allowed_ips {
            assert!(
                net.octets()[0] == 100 && (64..=127).contains(&net.octets()[1]),
                "{net}/{prefix} is not in Tailscale's 100.64.0.0/10"
            );
        }
        // Tailscale does not use WireGuard's pre-shared key, and inventing one
        // would be exactly the kind of guess this workspace does not make.
        assert_eq!(peer.preshared_key, None);
        assert_eq!(peer.persistent_keepalive, Some(25));
    }
}

/// RED-WHEN-BROKEN, and the subtlest thing on this wire: `AllowedIPs: null`
/// means **the same as `Addresses`**, not "no routes".
///
/// Read as an empty list, every one of these peers would be routed nothing —
/// a tunnel that comes up, reports healthy and silently drops every packet
/// meant for that node. The captured bytes have `"AllowedIPs": null` on all
/// five peers, which is exactly why they are the fixture.
#[test]
fn captured_null_allowed_ips_defaults_to_the_nodes_addresses() {
    assert!(
        MAP_RESPONSE.contains(r#""AllowedIPs": null"#),
        "the fixture must actually contain the null form, or this test proves nothing"
    );
    let set = parsed();
    let defaulted = set
        .findings
        .iter()
        .filter(|f| f.kind == NodeFindingKind::AllowedIpsDefaultedToAddresses)
        .count();
    assert_eq!(defaulted, 5, "all five peers hit the default: {:?}", set.findings);
    // And the default actually produced routes, rather than being noted and
    // then ignored — which would be a finding that reports a bug it did not fix.
    for peer in &set.peers {
        assert!(
            !peer.allowed_ips.is_empty(),
            "a defaulted AllowedIPs must yield the node's own address, not nothing"
        );
    }
}

/// RED-WHEN-BROKEN — the endpoint guess. Tailscale sends every candidate
/// because **disco** picks; with no disco this crate takes one and says so.
/// A silent choice here is a peer that never answers and no line anywhere
/// saying why.
#[test]
fn captured_multiple_endpoints_are_reported_as_a_guess_not_chosen_silently() {
    let set = parsed();
    let guesses: Vec<_> = set
        .findings
        .iter()
        .filter(|f| matches!(f.kind, NodeFindingKind::EndpointGuessed { .. }))
        .collect();
    assert!(
        !guesses.is_empty(),
        "the captured peers list several endpoints each: {:?}",
        set.findings
    );
    for f in &guesses {
        let NodeFindingKind::EndpointGuessed { candidates, .. } = f.kind else {
            unreachable!()
        };
        assert!(candidates > 1, "a single candidate is not a guess");
        assert!(f.marker_line().contains("no disco"), "{}", f.marker_line());
    }
}

/// The v6 half of every tailnet address is dropped, because the data plane
/// routes IPv4 only — and that must reach [`MissingPiece::Ipv6Routing`] rather
/// than staying a per-node note nobody aggregates.
#[test]
fn captured_ipv6_prefixes_are_dropped_and_that_reaches_the_reachability_verdict() {
    let set = parsed();
    assert!(
        set.findings
            .iter()
            .any(|f| matches!(f.kind, NodeFindingKind::Ipv6PrefixesDropped { .. })),
        "{:?}",
        set.findings
    );
    assert!(
        set.reachability.missing.contains(&MissingPiece::Ipv6Routing),
        "the per-node drop must reach the verdict: {:?}",
        set.reachability
    );
    // …and it is a WARNING, not a refusal: v4 still works.
    assert!(!MissingPiece::Ipv6Routing.is_fatal());
}

/// Offline peers are still built — they may come back — and still reported.
#[test]
fn captured_offline_peers_are_built_and_named() {
    let set = parsed();
    let offline = set
        .findings
        .iter()
        .filter(|f| f.kind == NodeFindingKind::Offline)
        .count();
    assert_eq!(offline, 2, "two peers are Online:false in the captured bytes");
    // They are in the peer set anyway: a node that is offline now is not a
    // node that will never return, and dropping it would make the peer set
    // wrong the moment it does.
    assert_eq!(set.peers.len(), 5);
}

/// **The refusal.** This is the crate's whole honest product: a peer set built
/// from a real netmap still cannot be turned into a tunnel that claims tailnet
/// membership, because a node with no DERP client and no disco is unreachable
/// rather than degraded.
///
/// RED-WHEN-BROKEN by construction — the day someone makes this return `Ok`,
/// an appliance starts reporting healthy while no peer will ever send to it.
#[test]
fn captured_a_real_netmap_still_refuses_to_become_a_tailnet_membership() {
    let set = parsed();
    assert!(!set.peers.is_empty(), "the refusal is about DERP, not about an empty netmap");
    assert!(!set.reachability.is_reachable());
    for fatal in [MissingPiece::DerpClient, MissingPiece::Disco, MissingPiece::ControlClient] {
        assert!(set.reachability.missing.contains(&fatal), "{:?}", set.reachability);
    }

    let err = set
        .into_tunnel_or_refuse([1u8; 32])
        .expect_err("a peer set without DERP must not become a tunnel");
    for expected in ["DERP is MANDATORY", "not a fallback", "disco", "carry nothing"] {
        assert!(err.contains(expected), "the refusal names {expected}: {err}");
    }
}

/// The identity guard for the test above: `into_tunnel_or_refuse` is not a
/// function that always says no. Given a reachable verdict it produces a
/// tunnel, so the refusal is about DERP and not about the function.
#[test]
fn the_refusal_is_about_derp_and_not_a_function_that_always_says_no() {
    let mut set = parsed();
    set.reachability.missing.retain(|m| !m.is_fatal());
    assert!(set.reachability.is_reachable());
    let peers = set.peers.len();
    let tunnel = set
        .into_tunnel_or_refuse([1u8; 32])
        .expect("with nothing fatal missing, a tunnel is produced");
    assert_eq!(tunnel.peers.len(), peers);
    assert_eq!(tunnel.listen_port, Some(41641));

    // …and an empty peer set is refused for its own reason, which is a
    // different sentence from the DERP one.
    let mut empty = parsed();
    empty.reachability.missing.retain(|m| !m.is_fatal());
    empty.peers.clear();
    let err = empty.into_tunnel_or_refuse([1u8; 32]).expect_err("no peers");
    assert!(err.contains("no usable peers"), "{err}");
}

/// A node whose key is the all-zero default must not become a peer. That key
/// is what a missing `Key` field deserialises to: 32 valid-looking bytes that
/// build a peer nothing can ever hand-shake with.
#[test]
fn captured_a_node_with_a_zero_key_is_refused_rather_than_built() {
    let zeroed = MAP_RESPONSE.replacen(
        "nodekey:a57413f6734a12fa6f50e600891517deef4d324ea63de8915da511550a20a23f",
        "nodekey:0000000000000000000000000000000000000000000000000000000000000000",
        1,
    );
    assert_ne!(zeroed, MAP_RESPONSE, "a key was actually zeroed");
    let response: MapResponse<'_> = serde_json::from_str(&zeroed).expect("parses");
    let set = peers_from_map_response(&response);
    assert_eq!(set.peers.len(), 4, "the zero-keyed node is not built");
    assert!(
        set.findings.iter().any(|f| f.kind == NodeFindingKind::UnusableKey),
        "{:?}",
        set.findings
    );
    // The identity guard: unmodified bytes still give five.
    assert_eq!(parsed().peers.len(), 5);
}

/// `Peers` takes precedence over `PeersChanged` when both are present — the
/// protocol says so, and reading the delta while the full list is there would
/// silently drop every peer that had not changed.
#[test]
fn captured_a_full_peers_list_takes_precedence_over_the_delta() {
    let as_full = MAP_RESPONSE.replacen(r#""PeersChanged": ["#, r#""Peers": ["#, 1);
    assert_ne!(as_full, MAP_RESPONSE);
    let response: MapResponse<'_> = serde_json::from_str(&as_full).expect("parses");
    let set = peers_from_map_response(&response);
    assert_eq!(set.peers.len(), 5, "the full list is read");

    // With NEITHER field, there are no peers at all — and that is reported as
    // a tailnet of one rather than as a parse failure.
    let empty: MapResponse<'_> = serde_json::from_str("{}").expect("an empty response parses");
    let set = peers_from_map_response(&empty);
    assert!(set.peers.is_empty());
    assert!(set.reachability.missing.contains(&MissingPiece::Peers));
    assert!(!MissingPiece::Peers.is_fatal(), "a tailnet of one is legitimate");
}