1pub const C2S_NET_OPEN: u8 = 0x80;
6pub const C2S_NET_DATA: u8 = 0x81;
8pub const C2S_NET_ACK: u8 = 0x82;
10pub const C2S_NET_CLOSE: u8 = 0x83;
12pub const C2S_NET_DGRAM: u8 = 0x84;
14
15pub const S2C_NET_OPENED: u8 = 0x80;
17pub const S2C_NET_DATA: u8 = 0x81;
19pub const S2C_NET_ACK: u8 = 0x82;
21pub const S2C_NET_CLOSED: u8 = 0x83;
23pub const S2C_NET_DGRAM: u8 = 0x84;
25
26pub const FEATURE_NET: u32 = 1 << 10;
28
29pub const NET_OPEN_TLS: u8 = 1 << 0;
33pub const NET_OPEN_INSECURE: u8 = 1 << 1;
35pub const NET_OPEN_UDP: u8 = 1 << 2;
37pub const NET_OPEN_FLAGS_KNOWN: u8 = NET_OPEN_TLS | NET_OPEN_INSECURE | NET_OPEN_UDP;
39
40pub const NET_CLOSE_WRITE: u8 = 1 << 0;
42
43pub const NET_STATUS_OK: u8 = 0;
47pub const NET_STATUS_UNKNOWN_ID: u8 = 1;
49pub const NET_STATUS_NOT_FOUND: u8 = 2;
51pub const NET_STATUS_REFUSED: u8 = 3;
53pub const NET_STATUS_PERMISSION: u8 = 4;
55pub const NET_STATUS_TLS: u8 = 5;
57pub const NET_STATUS_BUDGET: u8 = 6;
59pub const NET_STATUS_INVALID: u8 = 7;
61pub const NET_STATUS_OTHER: u8 = 9;
63
64pub 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
79pub const NET_CLOSED_EOF: u8 = 0;
81pub const NET_CLOSED_RESET: u8 = 1;
83pub const NET_CLOSED_TIMEOUT: u8 = 2;
85pub const NET_CLOSED_POLICY: u8 = 3;
87pub const NET_CLOSED_BUDGET: u8 = 4;
89pub const NET_CLOSED_SHUTDOWN: u8 = 5;
91
92pub 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
105pub const NET_MAX_HOST: usize = 255;
109pub const NET_MAX_CHUNK: usize = 64 * 1024;
111pub const NET_MAX_DGRAM: usize = 65507;
113pub const NET_WINDOW_BYTES: u64 = 1024 * 1024;
115pub const NET_WINDOW_AGGREGATE: u64 = 4 * 1024 * 1024;
117pub const NET_MAX_SOCKETS: usize = 256;
119pub const NET_DGRAM_QUEUE: usize = 256;
121
122#[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 pub sni: String,
133 pub alpn: Vec<String>,
135}
136
137impl NetOpen {
138 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 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 pub fn effective_sni(&self) -> &str {
172 if self.sni.is_empty() {
173 &self.host
174 } else {
175 &self.sni
176 }
177 }
178
179 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
229pub 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
274fn 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
283fn 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
364pub 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
386pub 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
419pub 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
433pub 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 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 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 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 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 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}