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}