Skip to main content

ts_packet/
geneve.rs

1//! Geneve (RFC 8926) fixed-header codec for Tailscale **peer-relay** framing.
2//!
3//! Tailscale's peer-relay data path encapsulates both relayed disco (the bind handshake) and
4//! relayed WireGuard data in a Geneve header carrying a 24-bit VNI (virtual network identifier).
5//! This module parses/encodes just the 8-byte fixed Geneve header Tailscale uses; the relay client
6//! that drives it lives in `ts_magicsock::relay`.
7//!
8//! Header layout (RFC 8926 §3.4, fixed 8 bytes; Tailscale uses no variable options):
9//!
10//! ```text
11//!  0                   1                   2                   3
12//!  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
13//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
14//! |Ver|  Opt Len  |O|C|    Rsvd.  |          Protocol Type        |
15//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
16//! |        Virtual Network Identifier (VNI)        |    Reserved   |
17//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
18//! ```
19
20/// The fixed Geneve header length in bytes (Tailscale uses no variable options, so `Opt Len` is 0).
21pub const GENEVE_FIXED_HEADER_LEN: usize = 8;
22
23/// Mask of the control-packet bit in byte 1: RFC 8926's **O** bit, which Go's
24/// `packet.GeneveHeader` calls `Control`.
25const CONTROL_BIT: u8 = 0x80;
26
27/// Geneve "Protocol Type" for relayed **disco** frames (Tailscale `GeneveProtocolDisco`).
28pub const GENEVE_PROTOCOL_DISCO: u16 = 0x7A11;
29/// Geneve "Protocol Type" for relayed **WireGuard** frames (Tailscale `GeneveProtocolWireGuard`).
30pub const GENEVE_PROTOCOL_WIREGUARD: u16 = 0x7A12;
31
32/// A parsed Tailscale Geneve fixed header.
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub struct GeneveHeader {
35    /// The control-packet bit: set when the payload is a control message (the relay bind
36    /// handshake) rather than tunneled data.
37    ///
38    /// This is RFC 8926's **O** bit — byte 1 bit 7, mask `0x80` — *not* the adjacent **C**
39    /// ("critical options present") bit at `0x40`. Go's `packet.GeneveHeader.Control` encodes and
40    /// decodes it at `0x80` (`net/packet/geneve.go`), and a UDP relay server dispatches on it:
41    /// `net/udprelay.Server` routes a datagram to its bind-handshake handler only when the bit is
42    /// set. Setting the wrong bit would make a Go relay treat this fork's handshake as tunneled
43    /// data and drop it, and would make us read a relay's challenge as data — no peer-relay path
44    /// could ever come up.
45    pub control: bool,
46    /// The inner protocol type (`GENEVE_PROTOCOL_DISCO` / `GENEVE_PROTOCOL_WIREGUARD`).
47    pub protocol: u16,
48    /// The 24-bit Virtual Network Identifier.
49    pub vni: u32,
50}
51
52/// Errors decoding a Geneve header.
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum GeneveError {
55    /// The buffer is shorter than the 8-byte fixed header.
56    TooShort,
57    /// The version field is not 0 (the only version Tailscale emits).
58    BadVersion,
59    /// A non-zero `Opt Len` was present (Tailscale uses no variable options; we don't parse them).
60    UnexpectedOptions,
61}
62
63impl GeneveHeader {
64    /// Parse a Geneve fixed header from the front of `buf`. Returns the header and the offset of the
65    /// inner payload (always [`GENEVE_FIXED_HEADER_LEN`]). Rejects a non-zero version or option
66    /// length (Tailscale emits neither) so a malformed/foreign Geneve packet is not mis-decoded.
67    pub fn parse(buf: &[u8]) -> Result<(GeneveHeader, usize), GeneveError> {
68        if buf.len() < GENEVE_FIXED_HEADER_LEN {
69            return Err(GeneveError::TooShort);
70        }
71        // Byte 0: Ver (2 bits) | Opt Len (6 bits, in 4-byte words).
72        let version = buf[0] >> 6;
73        if version != 0 {
74            return Err(GeneveError::BadVersion);
75        }
76        let opt_len_words = buf[0] & 0x3f;
77        if opt_len_words != 0 {
78            return Err(GeneveError::UnexpectedOptions);
79        }
80        // Byte 1: O (bit 7, the control-packet bit) | C (bit 6) | reserved.
81        let control = (buf[1] & CONTROL_BIT) != 0;
82        // Bytes 2..4: Protocol Type (big-endian u16).
83        let protocol = u16::from_be_bytes([buf[2], buf[3]]);
84        // Bytes 4..7: 24-bit VNI (big-endian); byte 7 is reserved.
85        let vni = (u32::from(buf[4]) << 16) | (u32::from(buf[5]) << 8) | u32::from(buf[6]);
86
87        Ok((
88            GeneveHeader {
89                control,
90                protocol,
91                vni,
92            },
93            GENEVE_FIXED_HEADER_LEN,
94        ))
95    }
96
97    /// Encode this header into an 8-byte fixed Geneve header (no variable options).
98    pub fn encode(&self) -> [u8; GENEVE_FIXED_HEADER_LEN] {
99        let mut out = [0u8; GENEVE_FIXED_HEADER_LEN];
100        // Ver = 0, Opt Len = 0 => byte 0 is 0.
101        out[0] = 0;
102        // Set the O (control-packet) bit when control; C and the reserved bits stay 0.
103        out[1] = if self.control { CONTROL_BIT } else { 0x00 };
104        out[2..4].copy_from_slice(&self.protocol.to_be_bytes());
105        out[4] = (self.vni >> 16) as u8;
106        out[5] = (self.vni >> 8) as u8;
107        out[6] = self.vni as u8;
108        // out[7] reserved = 0.
109        out
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    #[test]
118    fn roundtrip_disco_data() {
119        let h = GeneveHeader {
120            control: true,
121            protocol: GENEVE_PROTOCOL_DISCO,
122            vni: 0x0A_BC_DE,
123        };
124        let bytes = h.encode();
125        let (parsed, off) = GeneveHeader::parse(&bytes).unwrap();
126        assert_eq!(off, GENEVE_FIXED_HEADER_LEN);
127        assert_eq!(parsed, h);
128    }
129
130    #[test]
131    fn roundtrip_wireguard_no_control() {
132        let h = GeneveHeader {
133            control: false,
134            protocol: GENEVE_PROTOCOL_WIREGUARD,
135            vni: 1,
136        };
137        let bytes = h.encode();
138        assert_eq!(bytes[1] & 0x80, 0, "control bit must be clear");
139        let (parsed, _) = GeneveHeader::parse(&bytes).unwrap();
140        assert_eq!(parsed, h);
141    }
142
143    #[test]
144    fn vni_is_24_bits() {
145        let h = GeneveHeader {
146            control: false,
147            protocol: GENEVE_PROTOCOL_DISCO,
148            vni: 0xFF_FF_FF,
149        };
150        let bytes = h.encode();
151        // Byte 7 (reserved) must stay zero even at max VNI.
152        assert_eq!(bytes[7], 0);
153        let (parsed, _) = GeneveHeader::parse(&bytes).unwrap();
154        assert_eq!(parsed.vni, 0xFF_FF_FF);
155    }
156
157    #[test]
158    fn rejects_short_buffer() {
159        assert_eq!(GeneveHeader::parse(&[0u8; 7]), Err(GeneveError::TooShort));
160    }
161
162    #[test]
163    fn rejects_bad_version() {
164        let mut bytes = GeneveHeader {
165            control: false,
166            protocol: GENEVE_PROTOCOL_DISCO,
167            vni: 0,
168        }
169        .encode();
170        bytes[0] = 0x40; // version = 1
171        assert_eq!(GeneveHeader::parse(&bytes), Err(GeneveError::BadVersion));
172    }
173
174    #[test]
175    fn rejects_variable_options() {
176        let mut bytes = GeneveHeader {
177            control: false,
178            protocol: GENEVE_PROTOCOL_DISCO,
179            vni: 0,
180        }
181        .encode();
182        bytes[0] = 0x02; // opt len = 2 words
183        assert_eq!(
184            GeneveHeader::parse(&bytes),
185            Err(GeneveError::UnexpectedOptions)
186        );
187    }
188
189    #[test]
190    fn encode_matches_spec_byte_layout() {
191        // Byte-exact reference vector hand-derived from RFC 8926 §3.4 and cross-checked against
192        // Go's `packet.GeneveHeader.Encode` (`net/packet/geneve.go`), NOT computed by
193        // round-tripping through this fork's own encoder (that would be circular and would mask
194        // any byte-order / bit-position bug). The round-trip tests above only prove encode/parse
195        // are mutually consistent, not that either matches the wire format.
196        //
197        // For GeneveHeader { control: true, protocol: GENEVE_PROTOCOL_DISCO (0x7A11),
198        //                    vni: 0x0ABCDE }:
199        //   byte 0: Ver(2b)=00 | Opt Len(6b)=000000                     => 0x00
200        //   byte 1: O(bit7)=1 | C(bit6)=0 | Rsvd(6b)=0  (0b1000_0000)   => 0x80
201        //   byte 2: Protocol Type high byte (0x7A11 big-endian)          => 0x7A
202        //   byte 3: Protocol Type low  byte                              => 0x11
203        //   byte 4: VNI[23:16] of 0x0ABCDE                               => 0x0A
204        //   byte 5: VNI[15:8]                                            => 0xBC
205        //   byte 6: VNI[7:0]                                             => 0xDE
206        //   byte 7: Reserved                                             => 0x00
207        //
208        // Byte 1 is the one this fork got wrong before: Go writes the control-packet flag with
209        // `b[1] |= 0x80` and reads it with `b[1]&0x80 != 0`, and `net/udprelay.Server` dispatches
210        // a datagram to its bind-handshake handler only on that bit. Encoding it at 0x40 (the
211        // adjacent RFC "critical options" bit) made every relay handshake message look like
212        // tunneled data to a Go relay server, so no peer-relay path could come up.
213        //
214        // Residual gap: this is a SPEC- and source-derived vector, not one captured from a live
215        // Go `tailscaled` peer-relay packet.
216        let h = GeneveHeader {
217            control: true,
218            protocol: GENEVE_PROTOCOL_DISCO,
219            vni: 0x0A_BC_DE,
220        };
221        assert_eq!(h.encode(), [0x00, 0x80, 0x7A, 0x11, 0x0A, 0xBC, 0xDE, 0x00]);
222    }
223
224    #[test]
225    fn parse_known_wire_bytes() {
226        // Hand-built wire bytes (NOT produced by this fork's encoder), decoded field-by-field
227        // per RFC 8926 §3.4:
228        //   byte 0 = 0x00: Ver=00 (ok), Opt Len=000000 (no options)
229        //   byte 1 = 0x00: O(bit7)=0  => control = false (Go: `b[1]&0x80 != 0`)
230        //   bytes 2..4 = 0x7A,0x12: Protocol Type big-endian 0x7A12 = GENEVE_PROTOCOL_WIREGUARD
231        //   bytes 4..7 = 0x00,0x00,0x01: 24-bit VNI big-endian = 0x000001 = 1
232        //   byte 7 = 0x00: Reserved
233        // Inner payload therefore begins at offset GENEVE_FIXED_HEADER_LEN (8).
234        //
235        // Residual gap: spec- and source-derived, not captured from a live Go `tailscaled`.
236        let wire = [0x00, 0x00, 0x7A, 0x12, 0x00, 0x00, 0x01, 0x00];
237        let (parsed, off) = GeneveHeader::parse(&wire).unwrap();
238        assert_eq!(
239            parsed,
240            GeneveHeader {
241                control: false,
242                protocol: GENEVE_PROTOCOL_WIREGUARD,
243                vni: 1,
244            }
245        );
246        assert_eq!(off, GENEVE_FIXED_HEADER_LEN);
247    }
248
249    #[test]
250    fn parse_returns_payload_offset() {
251        let mut buf = GeneveHeader {
252            control: false,
253            protocol: GENEVE_PROTOCOL_WIREGUARD,
254            vni: 7,
255        }
256        .encode()
257        .to_vec();
258        buf.extend_from_slice(b"payload");
259        let (_, off) = GeneveHeader::parse(&buf).unwrap();
260        assert_eq!(&buf[off..], b"payload");
261    }
262
263    /// The control-packet flag must sit on RFC 8926's **O** bit (`0x80`), byte-for-byte where Go
264    /// puts it — `net/packet/geneve.go` writes `b[1] |= 0x80` and reads `b[1]&0x80 != 0`.
265    ///
266    /// This is an interop assertion, not a style one. `net/udprelay.Server` hands a datagram to
267    /// its bind-handshake handler only when it decodes this bit as set, and magicsock's receive
268    /// demux uses it to decide whether an inbound Geneve-wrapped disco frame is a relay handshake
269    /// message at all. Off by one bit and the whole peer-relay path is dead in both directions,
270    /// while every round-trip test in this file still passes.
271    #[test]
272    fn control_flag_is_the_o_bit_at_0x80() {
273        let control = GeneveHeader {
274            control: true,
275            protocol: GENEVE_PROTOCOL_DISCO,
276            vni: 1,
277        }
278        .encode();
279        assert_eq!(
280            control[1], 0x80,
281            "Go writes the control flag as b[1] |= 0x80"
282        );
283
284        // Decoding Go's byte must give control = true, and the neighbouring C bit must not.
285        let (from_go, _) = GeneveHeader::parse(&[0x00, 0x80, 0x7A, 0x11, 0, 0, 1, 0]).unwrap();
286        assert!(from_go.control);
287        let (c_bit_only, _) = GeneveHeader::parse(&[0x00, 0x40, 0x7A, 0x11, 0, 0, 1, 0]).unwrap();
288        assert!(
289            !c_bit_only.control,
290            "0x40 is the RFC 'critical options' C bit, not Go's control flag"
291        );
292    }
293}