Skip to main content

blit_remote/
net.rs

1//! TCP and UDP relay wire protocol (docs/design/net.md).
2//! A raw socket relay: the client names a host and port, the server opens a socket, and the two ends shuttle payload.
3
4/// Open a socket: [0x80][stream_id:2][flags:1][port:2][host_len:2][host:N] + TLS block The TLS block ([sni_len:2][sni:N][alpn_count:1] repeated{[proto_len:1][proto:N]}) is present iff [`NET_OPEN_TLS`] is set.
5pub const C2S_NET_OPEN: u8 = 0x80;
6/// Stream payload, TCP only: [0x81][stream_id:2][data:N]
7pub const C2S_NET_DATA: u8 = 0x81;
8/// Cumulative byte-window credit, TCP only: [0x82][stream_id:2][bytes:8]
9pub const C2S_NET_ACK: u8 = 0x82;
10/// Close or half-close: [0x83][stream_id:2][flags:1]
11pub const C2S_NET_CLOSE: u8 = 0x83;
12/// One datagram, UDP only: [0x84][stream_id:2][payload:N]
13pub const C2S_NET_DGRAM: u8 = 0x84;
14
15/// Open result, one per `NET_OPEN`: [0x80][stream_id:2][status:1][alpn_len:1][alpn:N][detail_len:2][detail:N]
16pub const S2C_NET_OPENED: u8 = 0x80;
17/// Stream payload, TCP only: [0x81][stream_id:2][data:N]
18pub const S2C_NET_DATA: u8 = 0x81;
19/// Cumulative byte-window credit, TCP only: [0x82][stream_id:2][bytes:8]
20pub const S2C_NET_ACK: u8 = 0x82;
21/// Socket ended: [0x83][stream_id:2][reason:1][detail_len:2][detail:N] On a UDP flow `detail` carries the drop counts, both directions.
22pub const S2C_NET_CLOSED: u8 = 0x83;
23/// One datagram, UDP only: [0x84][stream_id:2][payload:N]
24pub const S2C_NET_DGRAM: u8 = 0x84;
25
26/// `S2C_HELLO` feature bit: server supports the `NET_*` family (docs/design/net.md).
27pub const FEATURE_NET: u32 = 1 << 10;
28
29// --------------------------------------------------------------------------- Flags ---------------------------------------------------------------------------
30
31/// `NET_OPEN.flags` bit 0: terminate TLS toward the target; relayed bytes are the plaintext stream.
32pub const NET_OPEN_TLS: u8 = 1 << 0;
33/// `NET_OPEN.flags` bit 1: skip certificate and hostname verification.
34pub const NET_OPEN_INSECURE: u8 = 1 << 1;
35/// `NET_OPEN.flags` bit 2: open a UDP datagram flow rather than a TCP stream.
36pub const NET_OPEN_UDP: u8 = 1 << 2;
37/// Every defined `NET_OPEN` flag; anything else is `INVALID`.
38pub const NET_OPEN_FLAGS_KNOWN: u8 = NET_OPEN_TLS | NET_OPEN_INSECURE | NET_OPEN_UDP;
39
40/// `NET_CLOSE.flags` bit 0: shut down the client's write side only, leaving the stream readable.
41pub const NET_CLOSE_WRITE: u8 = 1 << 0;
42
43// --------------------------------------------------------------------------- Statuses and reasons ---------------------------------------------------------------------------
44
45/// `NET_OPENED.status`.
46pub const NET_STATUS_OK: u8 = 0;
47/// `stream_id` unknown or already closed.
48pub const NET_STATUS_UNKNOWN_ID: u8 = 1;
49/// `host` did not resolve.
50pub const NET_STATUS_NOT_FOUND: u8 = 2;
51/// Connect refused, unreachable, or timed out.
52pub const NET_STATUS_REFUSED: u8 = 3;
53/// Target refused by policy (docs/design/net.md § Target policy).
54pub const NET_STATUS_PERMISSION: u8 = 4;
55/// TLS handshake or certificate verification failed.
56pub const NET_STATUS_TLS: u8 = 5;
57/// Concurrent-socket or memory budget exhausted.
58pub const NET_STATUS_BUDGET: u8 = 6;
59/// Malformed request: unknown flags, empty host, live `stream_id`, `INSECURE` without `TLS`, TLS block absent with `TLS` set, `UDP` combined with `TLS` or `INSECURE`.
60pub const NET_STATUS_INVALID: u8 = 7;
61/// Anything else; diagnostic in `detail`.
62pub const NET_STATUS_OTHER: u8 = 9;
63
64/// Human-readable form of a `NET_OPENED.status`, for CLI diagnostics.
65pub fn net_status_text(status: u8) -> &'static str {
66    match status {
67        NET_STATUS_OK => "ok",
68        NET_STATUS_UNKNOWN_ID => "unknown stream id",
69        NET_STATUS_NOT_FOUND => "host did not resolve",
70        NET_STATUS_REFUSED => "connection refused",
71        NET_STATUS_PERMISSION => "refused by policy",
72        NET_STATUS_TLS => "TLS failed",
73        NET_STATUS_BUDGET => "budget exhausted",
74        NET_STATUS_INVALID => "invalid request",
75        _ => "error",
76    }
77}
78
79/// `NET_CLOSED.reason`: the target closed cleanly (TCP).
80pub const NET_CLOSED_EOF: u8 = 0;
81/// Connection reset by the target, or ICMP unreachable on a UDP flow.
82pub const NET_CLOSED_RESET: u8 = 1;
83/// Idle timeout — the normal end of a UDP flow.
84pub const NET_CLOSED_TIMEOUT: u8 = 2;
85/// Closed by the server: policy reload, target revoked.
86pub const NET_CLOSED_POLICY: u8 = 3;
87/// Retention or stream budget exceeded.
88pub const NET_CLOSED_BUDGET: u8 = 4;
89/// Server or blit connection going away.
90pub const NET_CLOSED_SHUTDOWN: u8 = 5;
91
92/// Human-readable form of a `NET_CLOSED.reason`, for CLI diagnostics.
93pub fn net_closed_text(reason: u8) -> &'static str {
94    match reason {
95        NET_CLOSED_EOF => "closed",
96        NET_CLOSED_RESET => "reset",
97        NET_CLOSED_TIMEOUT => "idle timeout",
98        NET_CLOSED_POLICY => "refused by policy",
99        NET_CLOSED_BUDGET => "budget exceeded",
100        NET_CLOSED_SHUTDOWN => "server going away",
101        _ => "ended",
102    }
103}
104
105// --------------------------------------------------------------------------- Limits ---------------------------------------------------------------------------
106
107/// Maximum `host` length in bytes.
108pub const NET_MAX_HOST: usize = 255;
109/// Maximum `NET_DATA`/`NET_DGRAM` payload.
110pub const NET_MAX_CHUNK: usize = 64 * 1024;
111/// Maximum UDP payload — UDP's own limit, which fits inside [`NET_MAX_CHUNK`].
112pub const NET_MAX_DGRAM: usize = 65507;
113/// Default per-stream unacked-byte window.
114pub const NET_WINDOW_BYTES: u64 = 1024 * 1024;
115/// Default aggregate unacked-byte window across all of a connection's streams, so N streams cannot each claim a full window.
116pub const NET_WINDOW_AGGREGATE: u64 = 4 * 1024 * 1024;
117/// Maximum concurrent sockets per blit connection, streams and flows together.
118pub const NET_MAX_SOCKETS: usize = 256;
119/// Per-direction datagram queue depth on a UDP flow.
120pub const NET_DGRAM_QUEUE: usize = 256;
121
122// --------------------------------------------------------------------------- Message builders and parsers ---------------------------------------------------------------------------
123
124/// A parsed `C2S_NET_OPEN`.
125#[derive(Clone, Debug, PartialEq, Eq)]
126pub struct NetOpen {
127    pub stream_id: u16,
128    pub flags: u8,
129    pub port: u16,
130    pub host: String,
131    /// Server Name Indication; empty means "use `host`".
132    pub sni: String,
133    /// ALPN protocols offered, in preference order.
134    pub alpn: Vec<String>,
135}
136
137impl NetOpen {
138    /// A plain TCP open: no TLS, no ALPN.
139    pub fn tcp(stream_id: u16, host: &str, port: u16) -> Self {
140        Self {
141            stream_id,
142            flags: 0,
143            port,
144            host: host.to_string(),
145            sni: String::new(),
146            alpn: Vec::new(),
147        }
148    }
149
150    /// A UDP flow open.
151    pub fn udp(stream_id: u16, host: &str, port: u16) -> Self {
152        Self {
153            stream_id,
154            flags: NET_OPEN_UDP,
155            port,
156            host: host.to_string(),
157            sni: String::new(),
158            alpn: Vec::new(),
159        }
160    }
161
162    pub fn is_udp(&self) -> bool {
163        self.flags & NET_OPEN_UDP != 0
164    }
165
166    pub fn is_tls(&self) -> bool {
167        self.flags & NET_OPEN_TLS != 0
168    }
169
170    /// The SNI to present: `sni` when set, otherwise `host`.
171    pub fn effective_sni(&self) -> &str {
172        if self.sni.is_empty() {
173            &self.host
174        } else {
175            &self.sni
176        }
177    }
178
179    /// Reject flag combinations the wire forbids, returning the `detail` for an `INVALID` reply.
180    pub fn validate(&self) -> Result<(), &'static str> {
181        if self.flags & !NET_OPEN_FLAGS_KNOWN != 0 {
182            return Err("unknown flags");
183        }
184        if self.host.is_empty() {
185            return Err("empty host");
186        }
187        if self.host.len() > NET_MAX_HOST {
188            return Err("host too long");
189        }
190        if self.host.contains('\0') {
191            return Err("host contains NUL");
192        }
193        if self.port == 0 {
194            return Err("port must be non-zero");
195        }
196        if self.flags & NET_OPEN_INSECURE != 0 && self.flags & NET_OPEN_TLS == 0 {
197            return Err("INSECURE without TLS");
198        }
199        if self.is_udp() && self.flags & (NET_OPEN_TLS | NET_OPEN_INSECURE) != 0 {
200            return Err("UDP with TLS");
201        }
202        Ok(())
203    }
204}
205
206pub fn msg_net_open(o: &NetOpen) -> Vec<u8> {
207    let hb = o.host.as_bytes();
208    let mut msg = Vec::with_capacity(8 + hb.len());
209    msg.push(C2S_NET_OPEN);
210    msg.extend_from_slice(&o.stream_id.to_le_bytes());
211    msg.push(o.flags);
212    msg.extend_from_slice(&o.port.to_le_bytes());
213    msg.extend_from_slice(&(hb.len() as u16).to_le_bytes());
214    msg.extend_from_slice(hb);
215    if o.flags & NET_OPEN_TLS != 0 {
216        let sb = o.sni.as_bytes();
217        msg.extend_from_slice(&(sb.len() as u16).to_le_bytes());
218        msg.extend_from_slice(sb);
219        msg.push(o.alpn.len().min(u8::MAX as usize) as u8);
220        for proto in o.alpn.iter().take(u8::MAX as usize) {
221            let pb = proto.as_bytes();
222            msg.push(pb.len().min(u8::MAX as usize) as u8);
223            msg.extend_from_slice(&pb[..pb.len().min(u8::MAX as usize)]);
224        }
225    }
226    msg
227}
228
229/// Parse a `C2S_NET_OPEN`.
230pub fn parse_net_open(msg: &[u8]) -> Option<NetOpen> {
231    if msg.len() < 8 || msg[0] != C2S_NET_OPEN {
232        return None;
233    }
234    let stream_id = u16::from_le_bytes([msg[1], msg[2]]);
235    let flags = msg[3];
236    let port = u16::from_le_bytes([msg[4], msg[5]]);
237    let host_len = u16::from_le_bytes([msg[6], msg[7]]) as usize;
238    let host = std::str::from_utf8(msg.get(8..8 + host_len)?)
239        .ok()?
240        .to_string();
241    let mut rest = &msg[8 + host_len..];
242    let (sni, alpn) = if flags & NET_OPEN_TLS != 0 {
243        if rest.len() < 2 {
244            return None;
245        }
246        let sni_len = u16::from_le_bytes([rest[0], rest[1]]) as usize;
247        let sni = std::str::from_utf8(rest.get(2..2 + sni_len)?)
248            .ok()?
249            .to_string();
250        rest = &rest[2 + sni_len..];
251        let count = *rest.first()?;
252        rest = &rest[1..];
253        let mut alpn = Vec::with_capacity(count as usize);
254        for _ in 0..count {
255            let len = *rest.first()? as usize;
256            let proto = std::str::from_utf8(rest.get(1..1 + len)?).ok()?.to_string();
257            rest = &rest[1 + len..];
258            alpn.push(proto);
259        }
260        (sni, alpn)
261    } else {
262        (String::new(), Vec::new())
263    };
264    Some(NetOpen {
265        stream_id,
266        flags,
267        port,
268        host,
269        sni,
270        alpn,
271    })
272}
273
274/// `C2S_NET_DATA` / `S2C_NET_DATA`, or the datagram variants — one builder, since the four differ only in opcode.
275fn msg_payload(opcode: u8, stream_id: u16, payload: &[u8]) -> Vec<u8> {
276    let mut msg = Vec::with_capacity(3 + payload.len());
277    msg.push(opcode);
278    msg.extend_from_slice(&stream_id.to_le_bytes());
279    msg.extend_from_slice(payload);
280    msg
281}
282
283/// Split `[stream_id:2][payload:N]` off a data or datagram message.
284fn parse_payload(msg: &[u8], opcode: u8) -> Option<(u16, &[u8])> {
285    if msg.len() < 3 || msg[0] != opcode {
286        return None;
287    }
288    Some((u16::from_le_bytes([msg[1], msg[2]]), &msg[3..]))
289}
290
291pub fn msg_net_data_c2s(stream_id: u16, data: &[u8]) -> Vec<u8> {
292    msg_payload(C2S_NET_DATA, stream_id, data)
293}
294
295pub fn msg_net_data_s2c(stream_id: u16, data: &[u8]) -> Vec<u8> {
296    msg_payload(S2C_NET_DATA, stream_id, data)
297}
298
299pub fn parse_net_data_c2s(msg: &[u8]) -> Option<(u16, &[u8])> {
300    parse_payload(msg, C2S_NET_DATA)
301}
302
303pub fn parse_net_data_s2c(msg: &[u8]) -> Option<(u16, &[u8])> {
304    parse_payload(msg, S2C_NET_DATA)
305}
306
307pub fn msg_net_dgram_c2s(stream_id: u16, payload: &[u8]) -> Vec<u8> {
308    msg_payload(C2S_NET_DGRAM, stream_id, payload)
309}
310
311pub fn msg_net_dgram_s2c(stream_id: u16, payload: &[u8]) -> Vec<u8> {
312    msg_payload(S2C_NET_DGRAM, stream_id, payload)
313}
314
315pub fn parse_net_dgram_c2s(msg: &[u8]) -> Option<(u16, &[u8])> {
316    parse_payload(msg, C2S_NET_DGRAM)
317}
318
319pub fn parse_net_dgram_s2c(msg: &[u8]) -> Option<(u16, &[u8])> {
320    parse_payload(msg, S2C_NET_DGRAM)
321}
322
323fn msg_ack(opcode: u8, stream_id: u16, bytes: u64) -> Vec<u8> {
324    let mut msg = Vec::with_capacity(11);
325    msg.push(opcode);
326    msg.extend_from_slice(&stream_id.to_le_bytes());
327    msg.extend_from_slice(&bytes.to_le_bytes());
328    msg
329}
330
331fn parse_ack(msg: &[u8], opcode: u8) -> Option<(u16, u64)> {
332    if msg.len() < 11 || msg[0] != opcode {
333        return None;
334    }
335    let stream_id = u16::from_le_bytes([msg[1], msg[2]]);
336    let bytes = u64::from_le_bytes(msg[3..11].try_into().unwrap());
337    Some((stream_id, bytes))
338}
339
340pub fn msg_net_ack_c2s(stream_id: u16, bytes: u64) -> Vec<u8> {
341    msg_ack(C2S_NET_ACK, stream_id, bytes)
342}
343
344pub fn msg_net_ack_s2c(stream_id: u16, bytes: u64) -> Vec<u8> {
345    msg_ack(S2C_NET_ACK, stream_id, bytes)
346}
347
348pub fn parse_net_ack_c2s(msg: &[u8]) -> Option<(u16, u64)> {
349    parse_ack(msg, C2S_NET_ACK)
350}
351
352pub fn parse_net_ack_s2c(msg: &[u8]) -> Option<(u16, u64)> {
353    parse_ack(msg, S2C_NET_ACK)
354}
355
356pub fn msg_net_close(stream_id: u16, flags: u8) -> Vec<u8> {
357    let mut msg = Vec::with_capacity(4);
358    msg.push(C2S_NET_CLOSE);
359    msg.extend_from_slice(&stream_id.to_le_bytes());
360    msg.push(flags);
361    msg
362}
363
364/// Parse a `C2S_NET_CLOSE` → `(stream_id, flags)`.
365pub fn parse_net_close(msg: &[u8]) -> Option<(u16, u8)> {
366    if msg.len() < 4 || msg[0] != C2S_NET_CLOSE {
367        return None;
368    }
369    Some((u16::from_le_bytes([msg[1], msg[2]]), msg[3]))
370}
371
372pub fn msg_net_opened(stream_id: u16, status: u8, alpn: &str, detail: &str) -> Vec<u8> {
373    let ab = alpn.as_bytes();
374    let db = detail.as_bytes();
375    let mut msg = Vec::with_capacity(7 + ab.len() + db.len());
376    msg.push(S2C_NET_OPENED);
377    msg.extend_from_slice(&stream_id.to_le_bytes());
378    msg.push(status);
379    msg.push(ab.len().min(u8::MAX as usize) as u8);
380    msg.extend_from_slice(&ab[..ab.len().min(u8::MAX as usize)]);
381    msg.extend_from_slice(&(db.len() as u16).to_le_bytes());
382    msg.extend_from_slice(db);
383    msg
384}
385
386/// Parse an `S2C_NET_OPENED` → `(stream_id, status, alpn, detail)`.
387pub fn parse_net_opened(msg: &[u8]) -> Option<(u16, u8, String, String)> {
388    if msg.len() < 7 || msg[0] != S2C_NET_OPENED {
389        return None;
390    }
391    let stream_id = u16::from_le_bytes([msg[1], msg[2]]);
392    let status = msg[3];
393    let alpn_len = msg[4] as usize;
394    let alpn = std::str::from_utf8(msg.get(5..5 + alpn_len)?)
395        .ok()?
396        .to_string();
397    let rest = &msg[5 + alpn_len..];
398    if rest.len() < 2 {
399        return None;
400    }
401    let detail_len = u16::from_le_bytes([rest[0], rest[1]]) as usize;
402    let detail = std::str::from_utf8(rest.get(2..2 + detail_len)?)
403        .ok()?
404        .to_string();
405    Some((stream_id, status, alpn, detail))
406}
407
408pub fn msg_net_closed(stream_id: u16, reason: u8, detail: &str) -> Vec<u8> {
409    let db = detail.as_bytes();
410    let mut msg = Vec::with_capacity(6 + db.len());
411    msg.push(S2C_NET_CLOSED);
412    msg.extend_from_slice(&stream_id.to_le_bytes());
413    msg.push(reason);
414    msg.extend_from_slice(&(db.len() as u16).to_le_bytes());
415    msg.extend_from_slice(db);
416    msg
417}
418
419/// Parse an `S2C_NET_CLOSED` → `(stream_id, reason, detail)`.
420pub fn parse_net_closed(msg: &[u8]) -> Option<(u16, u8, String)> {
421    if msg.len() < 6 || msg[0] != S2C_NET_CLOSED {
422        return None;
423    }
424    let stream_id = u16::from_le_bytes([msg[1], msg[2]]);
425    let reason = msg[3];
426    let detail_len = u16::from_le_bytes([msg[4], msg[5]]) as usize;
427    let detail = std::str::from_utf8(msg.get(6..6 + detail_len)?)
428        .ok()?
429        .to_string();
430    Some((stream_id, reason, detail))
431}
432
433/// True for any C2S opcode in the family's `0x80` block, for dispatch.
434pub fn is_c2s_net(opcode: u8) -> bool {
435    (C2S_NET_OPEN..=C2S_NET_DGRAM).contains(&opcode)
436}
437
438#[cfg(test)]
439mod tests {
440    use super::*;
441
442    #[test]
443    fn open_roundtrip_plain() {
444        let o = NetOpen::tcp(7, "db.internal", 5432);
445        let parsed = parse_net_open(&msg_net_open(&o)).unwrap();
446        assert_eq!(parsed, o);
447        assert_eq!(parsed.validate(), Ok(()));
448        assert!(!parsed.is_udp());
449    }
450
451    #[test]
452    fn open_roundtrip_udp() {
453        let o = NetOpen::udp(1, "resolver.internal", 53);
454        let parsed = parse_net_open(&msg_net_open(&o)).unwrap();
455        assert_eq!(parsed, o);
456        assert!(parsed.is_udp());
457        assert_eq!(parsed.validate(), Ok(()));
458    }
459
460    #[test]
461    fn open_roundtrip_tls_with_alpn() {
462        let o = NetOpen {
463            stream_id: 9,
464            flags: NET_OPEN_TLS,
465            port: 443,
466            host: "example.test".into(),
467            sni: "other.test".into(),
468            alpn: vec!["h2".into(), "http/1.1".into()],
469        };
470        let parsed = parse_net_open(&msg_net_open(&o)).unwrap();
471        assert_eq!(parsed, o);
472        assert_eq!(parsed.effective_sni(), "other.test");
473    }
474
475    #[test]
476    fn empty_sni_falls_back_to_host() {
477        let o = NetOpen {
478            flags: NET_OPEN_TLS,
479            ..NetOpen::tcp(1, "example.test", 443)
480        };
481        assert_eq!(o.effective_sni(), "example.test");
482        assert_eq!(parse_net_open(&msg_net_open(&o)).unwrap(), o);
483    }
484
485    #[test]
486    fn tls_block_may_offer_no_alpn() {
487        let o = NetOpen {
488            flags: NET_OPEN_TLS,
489            ..NetOpen::tcp(2, "example.test", 443)
490        };
491        let parsed = parse_net_open(&msg_net_open(&o)).unwrap();
492        assert!(parsed.alpn.is_empty());
493    }
494
495    #[test]
496    fn truncated_tls_block_is_rejected() {
497        let o = NetOpen {
498            flags: NET_OPEN_TLS,
499            alpn: vec!["h2".into()],
500            ..NetOpen::tcp(3, "example.test", 443)
501        };
502        let full = msg_net_open(&o);
503        // Every prefix that cuts into the TLS block must fail rather than silently decode as "TLS with no ALPN".
504        for cut in 8 + "example.test".len()..full.len() {
505            assert!(
506                parse_net_open(&full[..cut]).is_none(),
507                "prefix of len {cut} parsed"
508            );
509        }
510        assert!(parse_net_open(&full).is_some());
511    }
512
513    #[test]
514    fn validate_rejects_bad_combinations() {
515        let udp_tls = NetOpen {
516            flags: NET_OPEN_UDP | NET_OPEN_TLS,
517            ..NetOpen::tcp(1, "h", 1)
518        };
519        assert_eq!(udp_tls.validate(), Err("UDP with TLS"));
520
521        let insecure_only = NetOpen {
522            flags: NET_OPEN_INSECURE,
523            ..NetOpen::tcp(1, "h", 1)
524        };
525        assert_eq!(insecure_only.validate(), Err("INSECURE without TLS"));
526
527        let unknown = NetOpen {
528            flags: 1 << 5,
529            ..NetOpen::tcp(1, "h", 1)
530        };
531        assert_eq!(unknown.validate(), Err("unknown flags"));
532
533        let empty_host = NetOpen::tcp(1, "", 80);
534        assert_eq!(empty_host.validate(), Err("empty host"));
535
536        let zero_port = NetOpen::tcp(1, "h", 0);
537        assert_eq!(zero_port.validate(), Err("port must be non-zero"));
538
539        let long_host = NetOpen::tcp(1, &"x".repeat(NET_MAX_HOST + 1), 80);
540        assert_eq!(long_host.validate(), Err("host too long"));
541    }
542
543    #[test]
544    fn data_and_dgram_roundtrip() {
545        assert_eq!(
546            parse_net_data_c2s(&msg_net_data_c2s(4, b"hello")).unwrap(),
547            (4, &b"hello"[..])
548        );
549        assert_eq!(
550            parse_net_data_s2c(&msg_net_data_s2c(4, b"hello")).unwrap(),
551            (4, &b"hello"[..])
552        );
553        assert_eq!(
554            parse_net_dgram_c2s(&msg_net_dgram_c2s(5, b"query")).unwrap(),
555            (5, &b"query"[..])
556        );
557        assert_eq!(
558            parse_net_dgram_s2c(&msg_net_dgram_s2c(5, b"reply")).unwrap(),
559            (5, &b"reply"[..])
560        );
561    }
562
563    #[test]
564    fn empty_payload_is_a_valid_datagram() {
565        // A zero-length UDP datagram is legal and must survive the round trip as itself, not as a parse failure.
566        let msg = msg_net_dgram_c2s(6, b"");
567        assert_eq!(parse_net_dgram_c2s(&msg).unwrap(), (6, &b""[..]));
568    }
569
570    #[test]
571    fn data_and_dgram_opcodes_do_not_cross_parse() {
572        // Stream and datagram payloads must never decode as each other: that confusion is the whole reason they have separate opcodes.
573        let data = msg_net_data_c2s(1, b"x");
574        assert!(parse_net_dgram_c2s(&data).is_none());
575        let dgram = msg_net_dgram_c2s(1, b"x");
576        assert!(parse_net_data_c2s(&dgram).is_none());
577        // Directions do share opcode numbers, as everywhere in the protocol — which way a message is going is context, not a tag.
578        assert_eq!(C2S_NET_DATA, S2C_NET_DATA);
579    }
580
581    #[test]
582    fn ack_roundtrip_beyond_32_bits() {
583        let big = u64::MAX - 3;
584        assert_eq!(
585            parse_net_ack_c2s(&msg_net_ack_c2s(2, big)).unwrap(),
586            (2, big)
587        );
588        assert_eq!(
589            parse_net_ack_s2c(&msg_net_ack_s2c(2, big)).unwrap(),
590            (2, big)
591        );
592    }
593
594    #[test]
595    fn close_roundtrip() {
596        assert_eq!(
597            parse_net_close(&msg_net_close(3, NET_CLOSE_WRITE)).unwrap(),
598            (3, NET_CLOSE_WRITE)
599        );
600        assert_eq!(parse_net_close(&msg_net_close(3, 0)).unwrap(), (3, 0));
601    }
602
603    #[test]
604    fn opened_roundtrip() {
605        let msg = msg_net_opened(8, NET_STATUS_OK, "h2", "");
606        assert_eq!(
607            parse_net_opened(&msg).unwrap(),
608            (8, NET_STATUS_OK, "h2".to_string(), String::new())
609        );
610        let msg = msg_net_opened(8, NET_STATUS_TLS, "", "unknown issuer");
611        assert_eq!(
612            parse_net_opened(&msg).unwrap(),
613            (
614                8,
615                NET_STATUS_TLS,
616                String::new(),
617                "unknown issuer".to_string()
618            )
619        );
620    }
621
622    #[test]
623    fn closed_roundtrip() {
624        let msg = msg_net_closed(2, NET_CLOSED_TIMEOUT, "dropped 3 up, 0 down");
625        assert_eq!(
626            parse_net_closed(&msg).unwrap(),
627            (2, NET_CLOSED_TIMEOUT, "dropped 3 up, 0 down".to_string())
628        );
629    }
630
631    #[test]
632    fn dispatch_range_covers_the_family_only() {
633        for op in [
634            C2S_NET_OPEN,
635            C2S_NET_DATA,
636            C2S_NET_ACK,
637            C2S_NET_CLOSE,
638            C2S_NET_DGRAM,
639        ] {
640            assert!(is_c2s_net(op));
641        }
642        assert!(!is_c2s_net(0x7F));
643        assert!(!is_c2s_net(0x85));
644        // The kv block below must stay outside the range.
645        assert!(!is_c2s_net(crate::kv::C2S_KV_FETCH));
646    }
647
648    #[test]
649    fn feature_bit_is_free() {
650        for taken in [
651            crate::fs::FEATURE_FS,
652            crate::git::FEATURE_GIT,
653            crate::lsp::FEATURE_LSP,
654            crate::kv::FEATURE_KV,
655        ] {
656            assert_eq!(FEATURE_NET & taken, 0);
657        }
658    }
659
660    #[test]
661    fn dgram_cap_fits_the_chunk_cap() {
662        const { assert!(NET_MAX_DGRAM <= NET_MAX_CHUNK) };
663    }
664}