Skip to main content

yip_rendezvous/
proto.rs

1//! Node-id derivation and the rendezvous wire `Message` codec.
2use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
3
4use blake2::digest::{Update, VariableOutput};
5use blake2::Blake2sVar;
6use yip_membership::Record;
7
8/// Domain separation so node-id can't collide with the mesh-address derivation.
9const DOMAIN: &[u8] = b"yip-rdv-v1";
10
11/// A rendezvous identity: `BLAKE2s(DOMAIN || pubkey)[..16]`. Distinct domain
12/// from `yipd`'s `node_addr` so the two derivations never coincide.
13pub type NodeId = [u8; 16];
14
15/// Derive a node's rendezvous id from its X25519 public key.
16pub fn node_id(pubkey: &[u8; 32]) -> NodeId {
17    let mut h = Blake2sVar::new(16).expect("16 is a valid blake2s output len");
18    h.update(DOMAIN);
19    h.update(pubkey);
20    let mut out = [0u8; 16];
21    h.finalize_variable(&mut out).expect("output len matches");
22    out
23}
24
25const DOMAIN_REG_AUTH: &[u8] = b"yip-rendezvous-v1-reg-auth";
26
27/// Compute a 128-bit BLAKE2s-MAC for authenticated rootless rendezvous registration.
28pub fn compute_register_mac(obf_key: &[u8; 16], node: &NodeId, counter: u64) -> [u8; 16] {
29    let mut h = Blake2sVar::new(16).expect("16 is a valid blake2s output len");
30    h.update(DOMAIN_REG_AUTH);
31    h.update(obf_key);
32    h.update(node);
33    h.update(&counter.to_be_bytes());
34    let mut out = [0u8; 16];
35    h.finalize_variable(&mut out).expect("len matches");
36    out
37}
38
39/// Constant-time verification of `mac` against the expected BLAKE2s-MAC.
40pub fn verify_register_mac(
41    obf_key: &[u8; 16],
42    node: &NodeId,
43    counter: u64,
44    mac: &[u8; 16],
45) -> bool {
46    use subtle::ConstantTimeEq;
47
48    let expected = compute_register_mac(obf_key, node, counter);
49    expected.ct_eq(mac).into()
50}
51
52/// Message-type discriminants (the only permitted `as u8` in this crate).
53#[repr(u8)]
54enum Tag {
55    Register = 0,
56    Lookup = 1,
57    PeerInfo = 2,
58    NotFound = 3,
59    PunchHint = 4,
60    RelaySend = 5,
61    RelayDeliver = 6,
62    RegisterSigned = 7,
63    RegisterAuth = 8,
64}
65
66/// A rendezvous/relay control message. See the 2b spec for direction/semantics.
67#[derive(Debug, Clone, PartialEq, Eq)]
68pub enum Message {
69    Register {
70        node: NodeId,
71        /// Monotonic per-node freshness counter (anti-replay). Strictly
72        /// increasing across a node's registrations; the relay rejects any
73        /// Register whose counter is not greater than the last seen.
74        counter: u64,
75    },
76    Lookup {
77        node: NodeId,
78    },
79    PeerInfo {
80        node: NodeId,
81        reflexive: SocketAddr,
82        /// The requested node's signed directory record, when the relay has
83        /// one on file. `None` for legacy datagrams (no trailing presence
84        /// byte) or when no record is available.
85        record: Option<Record>,
86    },
87    NotFound {
88        node: NodeId,
89    },
90    PunchHint {
91        node: NodeId,
92        reflexive: SocketAddr,
93    },
94    RelaySend {
95        src: NodeId,
96        dst: NodeId,
97        payload: Vec<u8>,
98    },
99    RelayDeliver {
100        src: NodeId,
101        payload: Vec<u8>,
102    },
103    /// A self-authenticating registration: the relay verifies `record`
104    /// itself rather than trusting an unauthenticated `counter`.
105    RegisterSigned {
106        record: Record,
107    },
108    /// A symmetric MAC-authenticated registration for rootless relay/rendezvous:
109    /// verified via `verify_register_mac` under `obf_psk` to prevent false upgrades.
110    RegisterAuth {
111        node: NodeId,
112        counter: u64,
113        mac: [u8; 16],
114    },
115}
116
117/// Encode `record` onto `out` as a big-endian `u16` length prefix followed
118/// by its `Record::encode` bytes, mirroring how `record_signing_body`
119/// length-prefixes the embedded cert (`Record::decode` requires an exact
120/// slice, so a self-delimiting length prefix is required to embed one
121/// record inside another message).
122fn put_record(out: &mut Vec<u8>, record: &Record) {
123    let mut bytes = Vec::new();
124    record.encode(&mut bytes);
125    let len = u16::try_from(bytes.len()).expect("record fits in u16");
126    out.extend_from_slice(&len.to_be_bytes());
127    out.extend_from_slice(&bytes);
128}
129
130/// Parse a length-prefixed record from the front of `buf`. Returns the
131/// record, or `None` if the length prefix, slice, or record contents are
132/// invalid/truncated (fail-closed).
133fn take_record(buf: &[u8]) -> Option<Record> {
134    let len = usize::from(u16::from_be_bytes(buf.get(..2)?.try_into().ok()?));
135    let record_bytes = buf.get(2..2 + len)?;
136    Record::decode(record_bytes)
137}
138
139fn put_addr(out: &mut Vec<u8>, addr: &SocketAddr) {
140    match addr.ip() {
141        IpAddr::V4(ip) => {
142            out.push(4);
143            out.extend_from_slice(&ip.octets());
144        }
145        IpAddr::V6(ip) => {
146            out.push(6);
147            out.extend_from_slice(&ip.octets());
148        }
149    }
150    out.extend_from_slice(&addr.port().to_be_bytes());
151}
152
153fn take_addr(buf: &[u8]) -> Option<(SocketAddr, usize)> {
154    let (&fam, rest) = buf.split_first()?;
155    let (ip, used): (IpAddr, usize) = match fam {
156        4 => {
157            let o: [u8; 4] = rest.get(..4)?.try_into().ok()?;
158            (IpAddr::V4(Ipv4Addr::from(o)), 4)
159        }
160        6 => {
161            let o: [u8; 16] = rest.get(..16)?.try_into().ok()?;
162            (IpAddr::V6(Ipv6Addr::from(o)), 16)
163        }
164        _ => return None,
165    };
166    let port_bytes: [u8; 2] = rest.get(used..used + 2)?.try_into().ok()?;
167    let port = u16::from_be_bytes(port_bytes);
168    Some((SocketAddr::new(ip, port), 1 + used + 2))
169}
170
171/// Serialize `msg` onto `out` (appends; caller clears if reusing).
172pub fn encode(msg: &Message, out: &mut Vec<u8>) {
173    match msg {
174        Message::Register { node, counter } => {
175            out.push(Tag::Register as u8);
176            out.extend_from_slice(node);
177            out.extend_from_slice(&counter.to_be_bytes());
178        }
179        Message::Lookup { node } => {
180            out.push(Tag::Lookup as u8);
181            out.extend_from_slice(node);
182        }
183        Message::PeerInfo {
184            node,
185            reflexive,
186            record,
187        } => {
188            out.push(Tag::PeerInfo as u8);
189            out.extend_from_slice(node);
190            put_addr(out, reflexive);
191            match record {
192                Some(r) => {
193                    out.push(1);
194                    put_record(out, r);
195                }
196                None => out.push(0),
197            }
198        }
199        Message::NotFound { node } => {
200            out.push(Tag::NotFound as u8);
201            out.extend_from_slice(node);
202        }
203        Message::PunchHint { node, reflexive } => {
204            out.push(Tag::PunchHint as u8);
205            out.extend_from_slice(node);
206            put_addr(out, reflexive);
207        }
208        Message::RelaySend { src, dst, payload } => {
209            out.push(Tag::RelaySend as u8);
210            out.extend_from_slice(src);
211            out.extend_from_slice(dst);
212            out.extend_from_slice(payload);
213        }
214        Message::RelayDeliver { src, payload } => {
215            out.push(Tag::RelayDeliver as u8);
216            out.extend_from_slice(src);
217            out.extend_from_slice(payload);
218        }
219        Message::RegisterSigned { record } => {
220            out.push(Tag::RegisterSigned as u8);
221            put_record(out, record);
222        }
223        Message::RegisterAuth { node, counter, mac } => {
224            out.push(Tag::RegisterAuth as u8);
225            out.extend_from_slice(node);
226            out.extend_from_slice(&counter.to_be_bytes());
227            out.extend_from_slice(mac);
228        }
229    }
230}
231
232/// Parse one datagram into a `Message`, or `None` if malformed/truncated.
233pub fn decode(buf: &[u8]) -> Option<Message> {
234    let (&tag, rest) = buf.split_first()?;
235    let node16 = |b: &[u8]| -> Option<NodeId> { b.get(..16)?.try_into().ok() };
236    match tag {
237        t if t == Tag::Register as u8 => {
238            let node = node16(rest)?;
239            let counter = u64::from_be_bytes(rest.get(16..24)?.try_into().ok()?);
240            Some(Message::Register { node, counter })
241        }
242        t if t == Tag::Lookup as u8 => Some(Message::Lookup {
243            node: node16(rest)?,
244        }),
245        t if t == Tag::NotFound as u8 => Some(Message::NotFound {
246            node: node16(rest)?,
247        }),
248        t if t == Tag::PeerInfo as u8 => {
249            let node = node16(rest)?;
250            let (reflexive, used) = take_addr(rest.get(16..)?)?;
251            let after_addr = rest.get(16 + used..)?;
252            let record = match after_addr.first() {
253                Some(0) => None,
254                Some(1) => {
255                    let record = take_record(after_addr.get(1..)?)?;
256                    Some(record)
257                }
258                Some(_) => return None,
259                // No trailing presence byte: a legacy datagram. Treat as
260                // `None` for backward compatibility.
261                None => None,
262            };
263            Some(Message::PeerInfo {
264                node,
265                reflexive,
266                record,
267            })
268        }
269        t if t == Tag::PunchHint as u8 => {
270            let node = node16(rest)?;
271            let (reflexive, _) = take_addr(rest.get(16..)?)?;
272            Some(Message::PunchHint { node, reflexive })
273        }
274        t if t == Tag::RelaySend as u8 => {
275            let src = node16(rest)?;
276            let dst = node16(rest.get(16..)?)?;
277            Some(Message::RelaySend {
278                src,
279                dst,
280                payload: rest.get(32..)?.to_vec(),
281            })
282        }
283        t if t == Tag::RelayDeliver as u8 => {
284            let src = node16(rest)?;
285            Some(Message::RelayDeliver {
286                src,
287                payload: rest.get(16..)?.to_vec(),
288            })
289        }
290        t if t == Tag::RegisterSigned as u8 => {
291            let record = take_record(rest)?;
292            Some(Message::RegisterSigned { record })
293        }
294        t if t == Tag::RegisterAuth as u8 => {
295            if rest.len() != 40 {
296                return None;
297            }
298            let node = node16(rest)?;
299            let counter = u64::from_be_bytes(rest.get(16..24)?.try_into().ok()?);
300            let mac: [u8; 16] = rest.get(24..40)?.try_into().ok()?;
301            Some(Message::RegisterAuth { node, counter, mac })
302        }
303        _ => None,
304    }
305}
306
307#[cfg(test)]
308mod tests {
309    use super::*;
310    use std::net::SocketAddr;
311
312    #[test]
313    fn node_id_is_deterministic_and_16_bytes() {
314        let pk = [7u8; 32];
315        let a = node_id(&pk);
316        assert_eq!(a.len(), 16);
317        assert_eq!(node_id(&pk), a);
318        assert_ne!(node_id(&pk), node_id(&[8u8; 32]));
319    }
320
321    fn roundtrip(msg: Message) {
322        let mut buf = Vec::new();
323        encode(&msg, &mut buf);
324        assert_eq!(decode(&buf), Some(msg));
325    }
326
327    #[test]
328    fn decode_rejects_invalid_address_family() {
329        // A PeerInfo whose address family byte is neither 4 nor 6 must be
330        // rejected (fail-closed), not misparsed.
331        let mut buf = vec![Tag::PeerInfo as u8];
332        buf.extend_from_slice(&[7u8; 16]); // node
333        buf.push(99); // invalid family (valid are 4 / 6)
334        buf.extend_from_slice(&[0u8; 4]);
335        assert_eq!(decode(&buf), None);
336    }
337
338    #[test]
339    fn all_messages_roundtrip() {
340        let n = [1u8; 16];
341        let v4: SocketAddr = "203.0.113.9:5000".parse().unwrap();
342        let v6: SocketAddr = "[2001:db8::1]:5000".parse().unwrap();
343        roundtrip(Message::Register {
344            node: n,
345            counter: 1,
346        });
347        roundtrip(Message::Lookup { node: n });
348        roundtrip(Message::PeerInfo {
349            node: n,
350            reflexive: v4,
351            record: None,
352        });
353        roundtrip(Message::PeerInfo {
354            node: n,
355            reflexive: v6,
356            record: None,
357        });
358        roundtrip(Message::NotFound { node: n });
359        roundtrip(Message::PunchHint {
360            node: n,
361            reflexive: v4,
362        });
363        roundtrip(Message::RelaySend {
364            src: [3u8; 16],
365            dst: n,
366            payload: vec![9, 8, 7],
367        });
368        roundtrip(Message::RelayDeliver {
369            src: n,
370            payload: vec![1, 2, 3, 4],
371        });
372        roundtrip(Message::RegisterAuth {
373            node: n,
374            counter: 42,
375            mac: [0xaa; 16],
376        });
377    }
378
379    #[test]
380    fn decode_rejects_garbage_and_truncation() {
381        assert_eq!(decode(&[]), None);
382        assert_eq!(decode(&[0xFF]), None); // unknown discriminant
383        let mut buf = Vec::new();
384        encode(
385            &Message::PeerInfo {
386                node: [2u8; 16],
387                reflexive: "1.2.3.4:5".parse().unwrap(),
388                record: None,
389            },
390            &mut buf,
391        );
392        // Cut 2 bytes: the trailing `record: None` presence byte plus the
393        // last byte of the port, so the address itself is genuinely
394        // truncated (not just the presence byte, which legacy decode
395        // tolerates).
396        buf.truncate(buf.len() - 2);
397        assert_eq!(decode(&buf), None); // truncated addr
398    }
399
400    #[test]
401    fn register_roundtrips_with_counter() {
402        let n = node_id(&[7u8; 32]);
403        let msg = Message::Register {
404            node: n,
405            counter: 0x0102_0304_0506_0708,
406        };
407        let mut buf = Vec::new();
408        encode(&msg, &mut buf);
409        assert_eq!(decode(&buf), Some(msg));
410    }
411
412    #[test]
413    fn register_truncated_counter_is_none() {
414        // tag(1) + node(16) + only 4 of the 8 counter bytes
415        let mut buf = vec![0u8]; // Tag::Register
416        buf.extend_from_slice(&[9u8; 16]);
417        buf.extend_from_slice(&[0u8; 4]);
418        assert_eq!(decode(&buf), None);
419    }
420
421    fn addr(s: &str) -> SocketAddr {
422        s.parse().unwrap()
423    }
424
425    /// Build a decode-valid `Record`: a CA-signed `Cert` binding a member
426    /// key, and the record itself signed with the member's record-signing
427    /// key. Mirrors `yip_membership::record`'s own `make_signed_record` test
428    /// fixture (not exposed outside that crate, so reconstructed here).
429    fn sample_record() -> Record {
430        use ed25519_dalek::{Signer, SigningKey};
431        use getrandom::SysRng;
432        use rand_core::UnwrapErr;
433        use yip_membership::cert::cert_signing_body;
434        use yip_membership::record::{record_signing_body, sign};
435        use yip_membership::{node_id, Cert};
436
437        let ca = SigningKey::generate(&mut UnwrapErr(SysRng));
438        let member_pubkey = [1u8; 32];
439        let member_sign_key = SigningKey::generate(&mut UnwrapErr(SysRng));
440        let member_sign_pubkey = member_sign_key.verifying_key().to_bytes();
441        let network_id = [7u8; 16];
442
443        let mut cert = Cert {
444            version: 1,
445            member_pubkey,
446            member_sign_pubkey,
447            network_id,
448            not_before: 100,
449            not_after: 200,
450            tags: vec![],
451            ca_sig: [0u8; 64],
452        };
453        cert.ca_sig = ca.sign(&cert_signing_body(&cert)).to_bytes();
454
455        let mut record = Record {
456            node_id: node_id(&member_pubkey),
457            cert,
458            endpoints: vec![addr("192.0.2.1:8080")],
459            seq: 1,
460            sig: [0u8; 64],
461        };
462        let body = record_signing_body(&record);
463        record.sig = sign(
464            &body,
465            member_sign_key.to_bytes().as_ref().try_into().unwrap(),
466        );
467        record
468    }
469
470    #[test]
471    fn register_signed_roundtrips() {
472        let rec = sample_record();
473        roundtrip(Message::RegisterSigned { record: rec });
474    }
475
476    #[test]
477    fn peerinfo_with_record_roundtrips() {
478        roundtrip(Message::PeerInfo {
479            node: [7u8; 16],
480            reflexive: addr("198.51.100.7:41000"),
481            record: Some(sample_record()),
482        });
483    }
484
485    #[test]
486    fn peerinfo_without_record_roundtrips_and_is_backward_compatible() {
487        roundtrip(Message::PeerInfo {
488            node: [7u8; 16],
489            reflexive: addr("198.51.100.7:41000"),
490            record: None,
491        });
492    }
493
494    #[test]
495    fn peerinfo_legacy_datagram_with_no_presence_byte_decodes_as_none() {
496        // A legacy encoder that predates the `record` field never writes the
497        // trailing presence byte at all. Decode must still accept it and
498        // treat it as `record: None`.
499        let mut buf = vec![Tag::PeerInfo as u8];
500        buf.extend_from_slice(&[7u8; 16]); // node
501        put_addr(&mut buf, &addr("198.51.100.7:41000")); // no presence byte after
502        assert_eq!(
503            decode(&buf),
504            Some(Message::PeerInfo {
505                node: [7u8; 16],
506                reflexive: addr("198.51.100.7:41000"),
507                record: None,
508            })
509        );
510    }
511
512    #[test]
513    fn register_signed_length_prefix_exceeding_buffer_is_none() {
514        // A `RegisterSigned` whose u16 length prefix claims more bytes than
515        // are actually present must fail closed, not panic.
516        let mut buf = vec![Tag::RegisterSigned as u8];
517        buf.extend_from_slice(&0xFFFFu16.to_be_bytes()); // claims 65535 bytes
518        buf.extend_from_slice(&[0u8; 4]); // far fewer bytes actually follow
519        assert_eq!(decode(&buf), None);
520    }
521
522    #[test]
523    fn peerinfo_record_length_prefix_exceeding_buffer_is_none() {
524        let mut buf = vec![Tag::PeerInfo as u8];
525        buf.extend_from_slice(&[7u8; 16]); // node
526        put_addr(&mut buf, &addr("198.51.100.7:41000"));
527        buf.push(1); // presence byte: record follows
528        buf.extend_from_slice(&0xFFFFu16.to_be_bytes()); // claims 65535 bytes
529        buf.extend_from_slice(&[0u8; 4]); // far fewer bytes actually follow
530        assert_eq!(decode(&buf), None);
531    }
532
533    #[test]
534    fn test_compute_register_mac_kat_and_verify() {
535        let key = [0x42u8; 16];
536        let node = node_id(&[0x11u8; 32]);
537        let counter = 12345u64;
538        let mac = compute_register_mac(&key, &node, counter);
539        assert_ne!(mac, [0u8; 16]);
540        assert!(verify_register_mac(&key, &node, counter, &mac));
541
542        // Tampered key
543        let bad_key = [0x43u8; 16];
544        assert!(!verify_register_mac(&bad_key, &node, counter, &mac));
545
546        // Tampered node
547        let bad_node = node_id(&[0x22u8; 32]);
548        assert!(!verify_register_mac(&key, &bad_node, counter, &mac));
549
550        // Tampered counter
551        assert!(!verify_register_mac(&key, &node, counter + 1, &mac));
552
553        // Tampered mac byte
554        let mut bad_mac = mac;
555        bad_mac[0] ^= 1;
556        assert!(!verify_register_mac(&key, &node, counter, &bad_mac));
557    }
558
559    #[test]
560    fn test_register_auth_encode_decode_roundtrip() {
561        let node = node_id(&[0x55u8; 32]);
562        let counter = 9999u64;
563        let mac = [0x77u8; 16];
564        let msg = Message::RegisterAuth { node, counter, mac };
565        let mut buf = Vec::new();
566        encode(&msg, &mut buf);
567        assert_eq!(buf.len(), 1 + 16 + 8 + 16);
568        assert_eq!(buf[0], 8);
569        let decoded = decode(&buf).expect("decodes RegisterAuth");
570        assert_eq!(decoded, msg);
571    }
572
573    #[test]
574    fn register_auth_rejects_trailing_garbage_or_truncation() {
575        let node = node_id(&[0x55u8; 32]);
576        let counter = 9999u64;
577        let mac = [0x77u8; 16];
578        let msg = Message::RegisterAuth { node, counter, mac };
579        let mut buf = Vec::new();
580        encode(&msg, &mut buf);
581        assert_eq!(decode(&buf), Some(msg));
582
583        // Trailing garbage
584        let mut with_trailing = buf.clone();
585        with_trailing.push(0x00);
586        assert_eq!(
587            decode(&with_trailing),
588            None,
589            "trailing garbage must be rejected"
590        );
591
592        // Truncation
593        let mut truncated = buf;
594        truncated.pop();
595        assert_eq!(
596            decode(&truncated),
597            None,
598            "truncated datagram must be rejected"
599        );
600    }
601
602    #[test]
603    fn decode_unknown_tag_returns_none() {
604        let mut buf = vec![99u8];
605        buf.extend_from_slice(&[0u8; 40]);
606        assert_eq!(decode(&buf), None);
607    }
608}