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 data
6//! path itself (the `relayManager` handshake + magicsock integration) is not yet implemented in this
7//! fork, but recognizing the framing keeps the fork wire-aware (e.g. so relayed frames can be
8//! classified rather than treated as opaque/undecodable).
9//!
10//! Header layout (RFC 8926 §3.4, fixed 8 bytes; Tailscale uses no variable options):
11//!
12//! ```text
13//!  0                   1                   2                   3
14//!  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
15//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
16//! |Ver|  Opt Len  |O|C|    Rsvd.  |          Protocol Type        |
17//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
18//! |        Virtual Network Identifier (VNI)        |    Reserved   |
19//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
20//! ```
21
22/// The fixed Geneve header length in bytes (Tailscale uses no variable options, so `Opt Len` is 0).
23pub const GENEVE_FIXED_HEADER_LEN: usize = 8;
24
25/// Geneve "Protocol Type" for relayed **disco** frames (Tailscale `GeneveProtocolDisco`).
26pub const GENEVE_PROTOCOL_DISCO: u16 = 0x7A11;
27/// Geneve "Protocol Type" for relayed **WireGuard** frames (Tailscale `GeneveProtocolWireGuard`).
28pub const GENEVE_PROTOCOL_WIREGUARD: u16 = 0x7A12;
29
30/// A parsed Tailscale Geneve fixed header.
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub struct GeneveHeader {
33    /// The "Control" (C) bit: set when the payload is a control message (the relay bind handshake)
34    /// rather than tunneled data.
35    pub control: bool,
36    /// The inner protocol type (`GENEVE_PROTOCOL_DISCO` / `GENEVE_PROTOCOL_WIREGUARD`).
37    pub protocol: u16,
38    /// The 24-bit Virtual Network Identifier.
39    pub vni: u32,
40}
41
42/// Errors decoding a Geneve header.
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum GeneveError {
45    /// The buffer is shorter than the 8-byte fixed header.
46    TooShort,
47    /// The version field is not 0 (the only version Tailscale emits).
48    BadVersion,
49    /// A non-zero `Opt Len` was present (Tailscale uses no variable options; we don't parse them).
50    UnexpectedOptions,
51}
52
53impl GeneveHeader {
54    /// Parse a Geneve fixed header from the front of `buf`. Returns the header and the offset of the
55    /// inner payload (always [`GENEVE_FIXED_HEADER_LEN`]). Rejects a non-zero version or option
56    /// length (Tailscale emits neither) so a malformed/foreign Geneve packet is not mis-decoded.
57    pub fn parse(buf: &[u8]) -> Result<(GeneveHeader, usize), GeneveError> {
58        if buf.len() < GENEVE_FIXED_HEADER_LEN {
59            return Err(GeneveError::TooShort);
60        }
61        // Byte 0: Ver (2 bits) | Opt Len (6 bits, in 4-byte words).
62        let version = buf[0] >> 6;
63        if version != 0 {
64            return Err(GeneveError::BadVersion);
65        }
66        let opt_len_words = buf[0] & 0x3f;
67        if opt_len_words != 0 {
68            return Err(GeneveError::UnexpectedOptions);
69        }
70        // Byte 1: O (bit 7) | C (bit 6) | reserved.
71        let control = (buf[1] & 0x40) != 0;
72        // Bytes 2..4: Protocol Type (big-endian u16).
73        let protocol = u16::from_be_bytes([buf[2], buf[3]]);
74        // Bytes 4..7: 24-bit VNI (big-endian); byte 7 is reserved.
75        let vni = (u32::from(buf[4]) << 16) | (u32::from(buf[5]) << 8) | u32::from(buf[6]);
76
77        Ok((
78            GeneveHeader {
79                control,
80                protocol,
81                vni,
82            },
83            GENEVE_FIXED_HEADER_LEN,
84        ))
85    }
86
87    /// Encode this header into an 8-byte fixed Geneve header (no variable options).
88    pub fn encode(&self) -> [u8; GENEVE_FIXED_HEADER_LEN] {
89        let mut out = [0u8; GENEVE_FIXED_HEADER_LEN];
90        // Ver = 0, Opt Len = 0 => byte 0 is 0.
91        out[0] = 0;
92        // O bit unused (0); set C bit when control.
93        out[1] = if self.control { 0x40 } else { 0x00 };
94        out[2..4].copy_from_slice(&self.protocol.to_be_bytes());
95        out[4] = (self.vni >> 16) as u8;
96        out[5] = (self.vni >> 8) as u8;
97        out[6] = self.vni as u8;
98        // out[7] reserved = 0.
99        out
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn roundtrip_disco_data() {
109        let h = GeneveHeader {
110            control: true,
111            protocol: GENEVE_PROTOCOL_DISCO,
112            vni: 0x0A_BC_DE,
113        };
114        let bytes = h.encode();
115        let (parsed, off) = GeneveHeader::parse(&bytes).unwrap();
116        assert_eq!(off, GENEVE_FIXED_HEADER_LEN);
117        assert_eq!(parsed, h);
118    }
119
120    #[test]
121    fn roundtrip_wireguard_no_control() {
122        let h = GeneveHeader {
123            control: false,
124            protocol: GENEVE_PROTOCOL_WIREGUARD,
125            vni: 1,
126        };
127        let bytes = h.encode();
128        assert_eq!(bytes[1] & 0x40, 0, "control bit must be clear");
129        let (parsed, _) = GeneveHeader::parse(&bytes).unwrap();
130        assert_eq!(parsed, h);
131    }
132
133    #[test]
134    fn vni_is_24_bits() {
135        let h = GeneveHeader {
136            control: false,
137            protocol: GENEVE_PROTOCOL_DISCO,
138            vni: 0xFF_FF_FF,
139        };
140        let bytes = h.encode();
141        // Byte 7 (reserved) must stay zero even at max VNI.
142        assert_eq!(bytes[7], 0);
143        let (parsed, _) = GeneveHeader::parse(&bytes).unwrap();
144        assert_eq!(parsed.vni, 0xFF_FF_FF);
145    }
146
147    #[test]
148    fn rejects_short_buffer() {
149        assert_eq!(GeneveHeader::parse(&[0u8; 7]), Err(GeneveError::TooShort));
150    }
151
152    #[test]
153    fn rejects_bad_version() {
154        let mut bytes = GeneveHeader {
155            control: false,
156            protocol: GENEVE_PROTOCOL_DISCO,
157            vni: 0,
158        }
159        .encode();
160        bytes[0] = 0x40; // version = 1
161        assert_eq!(GeneveHeader::parse(&bytes), Err(GeneveError::BadVersion));
162    }
163
164    #[test]
165    fn rejects_variable_options() {
166        let mut bytes = GeneveHeader {
167            control: false,
168            protocol: GENEVE_PROTOCOL_DISCO,
169            vni: 0,
170        }
171        .encode();
172        bytes[0] = 0x02; // opt len = 2 words
173        assert_eq!(
174            GeneveHeader::parse(&bytes),
175            Err(GeneveError::UnexpectedOptions)
176        );
177    }
178
179    #[test]
180    fn encode_matches_spec_byte_layout() {
181        // Byte-exact reference vector hand-derived from RFC 8926 §3.4 + Tailscale's usage,
182        // NOT computed by round-tripping through this fork's own encoder (that would be
183        // circular and would mask any byte-order / bit-position bug). The 6 round-trip tests
184        // above only prove encode/parse are mutually consistent, not that either matches the
185        // wire format.
186        //
187        // For GeneveHeader { control: true, protocol: GENEVE_PROTOCOL_DISCO (0x7A11),
188        //                    vni: 0x0ABCDE }:
189        //   byte 0: Ver(2b)=00 | Opt Len(6b)=000000                     => 0x00
190        //   byte 1: O(bit7)=0 | C(bit6)=1 | Rsvd(6b)=0  (0b0100_0000)   => 0x40
191        //   byte 2: Protocol Type high byte (0x7A11 big-endian)          => 0x7A
192        //   byte 3: Protocol Type low  byte                              => 0x11
193        //   byte 4: VNI[23:16] of 0x0ABCDE                               => 0x0A
194        //   byte 5: VNI[15:8]                                            => 0xBC
195        //   byte 6: VNI[7:0]                                             => 0xDE
196        //   byte 7: Reserved                                             => 0x00
197        //
198        // Residual gap: this is a SPEC-derived vector, not one captured from a live Go
199        // `tailscaled` peer-relay packet. Full Go cross-validation would require a captured
200        // on-wire Geneve frame from tailscaled and is left as the remaining verification step.
201        let h = GeneveHeader {
202            control: true,
203            protocol: GENEVE_PROTOCOL_DISCO,
204            vni: 0x0A_BC_DE,
205        };
206        assert_eq!(h.encode(), [0x00, 0x40, 0x7A, 0x11, 0x0A, 0xBC, 0xDE, 0x00]);
207    }
208
209    #[test]
210    fn parse_known_wire_bytes() {
211        // Hand-built wire bytes (NOT produced by this fork's encoder), decoded field-by-field
212        // per RFC 8926 §3.4:
213        //   byte 0 = 0x00: Ver=00 (ok), Opt Len=000000 (no options)
214        //   byte 1 = 0x00: O=0, C(bit6)=0  => control = false
215        //   bytes 2..4 = 0x7A,0x12: Protocol Type big-endian 0x7A12 = GENEVE_PROTOCOL_WIREGUARD
216        //   bytes 4..7 = 0x00,0x00,0x01: 24-bit VNI big-endian = 0x000001 = 1
217        //   byte 7 = 0x00: Reserved
218        // Inner payload therefore begins at offset GENEVE_FIXED_HEADER_LEN (8).
219        //
220        // Residual gap: spec-derived, not captured from Go `tailscaled`; a real captured
221        // peer-relay frame would be needed for full cross-implementation validation.
222        let wire = [0x00, 0x00, 0x7A, 0x12, 0x00, 0x00, 0x01, 0x00];
223        let (parsed, off) = GeneveHeader::parse(&wire).unwrap();
224        assert_eq!(
225            parsed,
226            GeneveHeader {
227                control: false,
228                protocol: GENEVE_PROTOCOL_WIREGUARD,
229                vni: 1,
230            }
231        );
232        assert_eq!(off, GENEVE_FIXED_HEADER_LEN);
233    }
234
235    #[test]
236    fn parse_returns_payload_offset() {
237        let mut buf = GeneveHeader {
238            control: false,
239            protocol: GENEVE_PROTOCOL_WIREGUARD,
240            vni: 7,
241        }
242        .encode()
243        .to_vec();
244        buf.extend_from_slice(b"payload");
245        let (_, off) = GeneveHeader::parse(&buf).unwrap();
246        assert_eq!(&buf[off..], b"payload");
247    }
248}