pktkit 0.1.2

Zero-copy L2/L3 packet handling toolkit. Frames, packets, hubs, adapters, NAT, virtual TCP/IP, WireGuard, OpenVPN, QEMU networking, TUN/TAP, AF_XDP — all gated behind opt-in cargo features.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
//! Per-peer state machine.
//!
//! A [`Peer`] drives one OpenVPN client through:
//!
//! 1. **Hard reset** — the client's `P_CONTROL_HARD_RESET_CLIENT_V2` sets the
//!    peer session id; we reply with our own server hard reset.
//! 2. **TLS handshake** — a `rustls::ServerConnection` runs *inside* the
//!    reliable control channel. We feed it the TLS bytes carried by
//!    `P_CONTROL_V1` packets (`read_tls` + `process_new_packets`) and pump its
//!    output back out as more `P_CONTROL_V1` packets (`write_tls`). There is no
//!    TCP socket under the TLS; the reliable layer is the transport.
//! 3. **Key-method 2 exchange** — over the established TLS stream the client
//!    sends `[0:4][key_method:1][pre_master:48][random1:32][random2:32]` plus
//!    the options, username, password, and peer-info strings. We reply
//!    symmetrically, then derive the data-channel keys with the TLS-1.0 PRF.
//! 4. **Data channel** — `P_DATA_V1` packets are decrypted to IP packets (tun)
//!    or Ethernet frames (tap) and delivered to the adapter; outgoing packets
//!    are encrypted and emitted.
//!
//! Ported from the Go `peer.go` + `peer-control.go` + `peerconn.go`. The Go
//! version used blocking reads on a `tls.Conn` from a dedicated goroutine; the
//! Rust port is single-threaded and event-driven — each inbound datagram is
//! processed synchronously and any work that can make progress does so.

use std::io::{self, Read, Write};
use std::sync::Arc;

use rustls::ServerConnection;

use super::consts::{KEY_EXPANSION_ID, KEY_METHOD_MASK};
use super::data;
use super::keys::PeerKeys;
use super::options::Options;
use super::prf::prf10;
use super::reliable::Reliable;
use super::window::Window;
use super::Opcode;

fn invalid(msg: impl Into<String>) -> io::Error {
    io::Error::new(io::ErrorKind::InvalidData, msg.into())
}

/// Credentials and metadata presented by a client during the key exchange.
#[derive(Debug, Clone)]
pub struct AuthInfo {
    pub username: String,
    pub password: String,
    pub peer_info: std::collections::HashMap<String, String>,
    pub dev_type: String,
}

/// IP configuration the server pushes back to an authenticated client.
#[derive(Debug, Clone)]
pub struct PeerConfig {
    /// Tunnel address assigned to the client.
    pub ip: std::net::IpAddr,
    /// Peer/gateway address used in the net30 topology push (tun mode).
    pub gateway: std::net::IpAddr,
    /// Netmask string used for tap-mode ifconfig.
    pub mask: std::net::IpAddr,
    /// Prefix length for the per-peer device address.
    pub prefix_len: u8,
}

/// Authentication callback: given the credentials, return the IP config to push
/// or an error to reject the connection.
pub type OnAuth = Arc<dyn Fn(&AuthInfo) -> io::Result<PeerConfig> + Send + Sync>;

/// Effects produced by processing one inbound datagram: raw datagrams to send
/// back to the peer, and an optional decrypted data-channel payload to deliver.
#[derive(Default, Debug)]
pub struct PeerOutput {
    /// Raw datagrams (each already framed with opcode etc.) to transmit.
    pub send: Vec<Vec<u8>>,
    /// Decrypted payload to deliver to the adapter, if a data packet arrived.
    pub deliver: Option<Vec<u8>>,
    /// True once the peer has authenticated and the data channel is active.
    pub authenticated: bool,
    /// True if the connection should be torn down.
    pub close: bool,
}

/// Phase of the control flow.
#[derive(Debug, PartialEq, Eq)]
enum Phase {
    /// Waiting for the client hard reset.
    Init,
    /// TLS handshake / key exchange in progress.
    Handshaking,
    /// Authenticated; data channel active.
    Established,
}

/// One OpenVPN peer.
pub struct Peer {
    tls: ServerConnection,
    reliable: Reliable,
    phase: Phase,

    on_auth: OnAuth,

    // Negotiated state, populated during the key exchange.
    opts: Option<Options>,
    keys: Option<PeerKeys>,
    replay: Window,
    /// Layer: 2 = tap (frames), 3 = tun (packets).
    layer: u8,
    /// Outgoing data-channel packet id.
    out_pid: u32,

    // Key-method 2 exchange scratch (read incrementally from the TLS stream).
    ctrl_buf: Vec<u8>,
    kx_done: bool,
    peer_cfg: Option<PeerConfig>,
    /// Peer-info key/values (`IV_*`) the client advertised during the key
    /// exchange, retained for post-auth queries / diagnostics.
    peer_info: std::collections::HashMap<String, String>,
    /// Server random material (r1||r2) generated for the key exchange and
    /// reused by [`derive_keys`] so the PRF inputs match what was sent.
    server_random: [u8; 64],
}

impl std::fmt::Debug for Peer {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Peer")
            .field("phase", &self.phase)
            .field("authenticated", &self.kx_done)
            .finish()
    }
}

impl Peer {
    /// Create a peer with the given rustls config, local session id, and auth
    /// hook. The local session id is typically random; the server uses it to
    /// identify reliable-layer packets.
    pub fn new(
        config: Arc<rustls::ServerConfig>,
        local_id: [u8; 8],
        on_auth: OnAuth,
    ) -> io::Result<Peer> {
        let tls = ServerConnection::new(config)
            .map_err(|e| invalid(format!("rustls server connection: {e}")))?;
        Ok(Peer {
            tls,
            reliable: Reliable::new(local_id),
            phase: Phase::Init,
            on_auth,
            opts: None,
            keys: None,
            replay: Window::new(),
            layer: 3,
            out_pid: 0,
            ctrl_buf: Vec::new(),
            kx_done: false,
            peer_cfg: None,
            peer_info: std::collections::HashMap::new(),
            server_random: [0u8; 64],
        })
    }

    /// The peer config pushed to the client after authentication (if any).
    pub fn peer_config(&self) -> Option<&PeerConfig> {
        self.peer_cfg.as_ref()
    }

    /// Layer (2 for tap, 3 for tun).
    pub fn layer(&self) -> u8 {
        self.layer
    }

    /// Drive periodic maintenance: retransmit any unacknowledged control
    /// packets whose deadline has passed at `now`.
    ///
    /// The OpenVPN reliable layer must re-send `P_CONTROL` packets that the
    /// peer has not ACKed within a timeout (~1s, backing off exponentially).
    /// Because the crate is caller-driven (no per-peer background thread), the
    /// server/[`Adapter`](super::Adapter) must call this on a timer — roughly
    /// once per retransmit interval (~1s) — for each live peer. Returns the
    /// datagrams to re-send in [`PeerOutput`]`::send`; if a packet exhausts its
    /// retries the connection is abandoned and `PeerOutput::close` is set.
    pub fn tick(&mut self, now: std::time::Instant) -> io::Result<PeerOutput> {
        let mut out = PeerOutput::default();
        let tick = self.reliable.tick(now);
        out.send = tick.resend;
        out.close = tick.timed_out;
        Ok(out)
    }

    /// Process one inbound datagram from the peer.
    pub fn handle_packet(&mut self, data: &[u8]) -> io::Result<PeerOutput> {
        if data.is_empty() {
            return Ok(PeerOutput::default());
        }
        let (opcode, _kid) = Opcode::from_byte(data[0]);

        if opcode.is_control() {
            self.handle_control(data)
        } else if opcode == Opcode::DATA_V1 || opcode == Opcode::DATA_V2 {
            self.handle_data(data)
        } else {
            Ok(PeerOutput::default())
        }
    }

    fn handle_control(&mut self, data: &[u8]) -> io::Result<PeerOutput> {
        let mut out = PeerOutput::default();

        let recv = self.reliable.recv(data)?;

        if recv.got_client_reset {
            if self.phase == Phase::Init {
                self.phase = Phase::Handshaking;
            }
            // Reply with a server hard reset, then ACK.
            let reset = self.reliable.build_hard_reset();
            let acks = self.reliable.take_pending_acks();
            out.send.push(reset.to_bytes(&acks));
            self.pump_tls(&mut out)?;
            return Ok(out);
        }

        // Feed any newly-ordered TLS bytes into rustls.
        if !recv.tls_bytes.is_empty() {
            let mut cursor = io::Cursor::new(&recv.tls_bytes);
            while (cursor.position() as usize) < recv.tls_bytes.len() {
                let n = self
                    .tls
                    .read_tls(&mut cursor)
                    .map_err(|e| invalid(format!("read_tls: {e}")))?;
                if n == 0 {
                    break;
                }
                self.tls
                    .process_new_packets()
                    .map_err(|e| invalid(format!("tls process: {e}")))?;
            }

            // Drain decrypted plaintext into the control buffer.
            let mut plain = Vec::new();
            let _ = self.tls.reader().read_to_end(&mut plain);
            if !plain.is_empty() {
                self.ctrl_buf.extend_from_slice(&plain);
            }

            // Try to advance the key-method-2 exchange / push handling.
            self.advance_control()?;
        }

        // Pump any TLS output (handshake records or our control replies) back
        // onto the reliable layer, then attach pending ACKs.
        self.pump_tls(&mut out)?;

        if self.kx_done {
            out.authenticated = true;
        }
        Ok(out)
    }

    /// Emit any pending TLS output as P_CONTROL_V1 packets, plus a standalone
    /// ACK if we owe acknowledgements but produced no control packet to ride on.
    fn pump_tls(&mut self, out: &mut PeerOutput) -> io::Result<()> {
        let mut tls_out = Vec::new();
        while self.tls.wants_write() {
            let n = self
                .tls
                .write_tls(&mut tls_out)
                .map_err(|e| invalid(format!("write_tls: {e}")))?;
            if n == 0 {
                break;
            }
        }

        if !tls_out.is_empty() {
            let chunks = self.reliable.chunk_tls_stream(&tls_out);
            for (i, pkt) in chunks.iter().enumerate() {
                // Attach pending acks only to the first packet of the burst.
                let acks = if i == 0 {
                    self.reliable.take_pending_acks()
                } else {
                    Vec::new()
                };
                out.send.push(pkt.to_bytes(&acks));
            }
        }

        // If we still owe acks (no control packet carried them), send a plain ACK.
        if self.reliable.has_pending_acks() {
            let acks = self.reliable.take_pending_acks();
            let ack = self.reliable.build_ack();
            out.send.push(ack.to_bytes(&acks));
        }
        Ok(())
    }

    /// Advance the key-method-2 control exchange using whatever plaintext bytes
    /// are buffered. Runs at most once (after which the connection only carries
    /// PUSH_REQUEST and data). Writes the server reply into the TLS writer.
    fn advance_control(&mut self) -> io::Result<()> {
        if self.kx_done {
            self.handle_post_auth_control()?;
            return Ok(());
        }

        // We need the full fixed prefix + four control strings before we can
        // respond. Parse non-destructively; bail (waiting for more) if short.
        let parsed = match self.try_parse_key_exchange()? {
            Some(p) => p,
            None => return Ok(()), // not enough bytes yet
        };

        // Generate the server random once; it's used both in the reply and in
        // the PRF key derivation.
        fill_random(&mut self.server_random)?;

        // Build the server reply onto the TLS stream.
        let reply = self.build_kx_reply(&parsed)?;
        self.tls
            .writer()
            .write_all(&reply)
            .map_err(|e| invalid(format!("tls write reply: {e}")))?;

        // Derive the data-channel keys.
        self.derive_keys(&parsed)?;

        // Authenticate via the hook.
        let auth = AuthInfo {
            username: parsed.username.clone(),
            password: parsed.password.clone(),
            peer_info: parsed.peer_info.clone(),
            dev_type: parsed.opts.dev_type.clone(),
        };
        let cfg = (self.on_auth)(&auth)?;
        self.peer_cfg = Some(cfg);

        self.layer = match parsed.opts.dev_type.as_str() {
            "tap" => 2,
            _ => 3,
        };
        self.peer_info = parsed.peer_info;
        self.opts = Some(parsed.opts);
        self.kx_done = true;
        self.phase = Phase::Established;
        Ok(())
    }

    /// After authentication the client keeps sending NUL-terminated control
    /// messages over the TLS stream. Mirrors the post-auth loop in the Go
    /// `peer-control.go`:
    ///
    /// - `PUSH_REQUEST` — (re)send the `PUSH_REPLY`. OpenVPN clients repeat the
    ///   request until they see a reply, so every occurrence must be answered,
    ///   not just the first.
    /// - everything else (`PING`, `INFO`, additional `PUSH_*`, etc.) is
    ///   gracefully ignored: we consume the message and keep the channel open.
    fn handle_post_auth_control(&mut self) -> io::Result<()> {
        while let Some(nul) = self.ctrl_buf.iter().position(|&b| b == 0) {
            let line: Vec<u8> = self.ctrl_buf.drain(..=nul).collect();
            // Drop the trailing NUL; empty (bare-NUL) keepalives are ignored.
            let body = &line[..line.len() - 1];
            if body.is_empty() {
                continue;
            }
            let s = String::from_utf8_lossy(body).into_owned();
            // OpenVPN control commands are comma-separated; the verb is the
            // first field (`PUSH_REQUEST`, `PUSH_UPDATE,...`, etc.). Only
            // `PUSH_REQUEST` is actioned; every other message (`PING`, `INFO`,
            // additional `PUSH_*`, …) is gracefully consumed and ignored,
            // matching the Go upstream's permissive post-auth loop.
            let verb = s.split(',').next().unwrap_or("");
            if verb == "PUSH_REQUEST" {
                let reply = self.build_push_reply();
                self.tls
                    .writer()
                    .write_all(reply.as_bytes())
                    .map_err(|e| invalid(format!("tls push reply: {e}")))?;
            }
        }
        Ok(())
    }

    /// Peer-info (`IV_*`) key/values the client advertised during the key
    /// exchange. Empty until the peer authenticates.
    pub fn peer_info(&self) -> &std::collections::HashMap<String, String> {
        &self.peer_info
    }

    fn build_push_reply(&self) -> String {
        let cfg = self.peer_cfg.as_ref();
        let (ip, gw_or_mask) = match cfg {
            Some(c) if self.layer == 2 => (c.ip.to_string(), c.mask.to_string()),
            Some(c) => (c.ip.to_string(), c.gateway.to_string()),
            None => ("0.0.0.0".to_string(), "0.0.0.0".to_string()),
        };
        format!(
            "PUSH_REPLY,ping 10,comp-lzo no,topology net30,ifconfig {} {}\0",
            ip, gw_or_mask
        )
    }

    // --- key-method 2 parsing -------------------------------------------------

    fn try_parse_key_exchange(&self) -> io::Result<Option<KeyExchange>> {
        let buf = &self.ctrl_buf;

        // Fixed prefix: 4 zero bytes, key_method, pre_master(48), r1(32), r2(32).
        let fixed = 4 + 1 + 48 + 32 + 32;
        if buf.len() < fixed {
            return Ok(None);
        }
        let zero = u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]);
        if zero != 0 {
            return Err(invalid("control channel: expected 4 zero bytes"));
        }
        let key_method = buf[4];
        if key_method & KEY_METHOD_MASK != 2 {
            return Err(invalid("invalid key method, expected method 2"));
        }
        let mut pos = 5;
        let mut pre_master = [0u8; 48];
        pre_master.copy_from_slice(&buf[pos..pos + 48]);
        pos += 48;
        let mut random1 = [0u8; 32];
        random1.copy_from_slice(&buf[pos..pos + 32]);
        pos += 32;
        let mut random2 = [0u8; 32];
        random2.copy_from_slice(&buf[pos..pos + 32]);
        pos += 32;

        // Four NUL-terminated, length-prefixed control strings.
        let (options_string, p1) = match read_control_string(buf, pos)? {
            Some(v) => v,
            None => return Ok(None),
        };
        let (username, p2) = match read_control_string(buf, p1)? {
            Some(v) => v,
            None => return Ok(None),
        };
        let (password, p3) = match read_control_string(buf, p2)? {
            Some(v) => v,
            None => return Ok(None),
        };
        let (peer_info_raw, _p4) = match read_control_string(buf, p3)? {
            Some(v) => v,
            None => return Ok(None),
        };

        // Validate the options string round-trips (as the Go upstream does).
        let mut opts = Options::parse(&options_string).map_err(invalid)?;
        opts.is_server = false;
        if opts.to_string() != options_string {
            return Err(invalid("invalid options provided"));
        }

        let peer_info = parse_peer_info(&peer_info_raw)?;

        let options_server = {
            let mut o = opts.clone();
            o.is_server = true;
            o.to_string()
        };
        // options_string was validated above (round-trip check); not retained.
        let _ = options_string;

        Ok(Some(KeyExchange {
            pre_master,
            random1,
            random2,
            options_server,
            opts,
            username,
            password,
            peer_info,
            peer_info_raw,
        }))
    }

    fn build_kx_reply(&self, kx: &KeyExchange) -> io::Result<Vec<u8>> {
        let mut buf = Vec::new();
        buf.extend_from_slice(&0u32.to_be_bytes());
        buf.push(2u8); // key_method

        // Server random material (generated in advance_control).
        buf.extend_from_slice(&self.server_random); // r1(32)||r2(32)

        write_control_string(&mut buf, &kx.options_server);
        write_control_string(&mut buf, ""); // username
        write_control_string(&mut buf, ""); // password
        write_control_string(&mut buf, &kx.peer_info_raw);
        Ok(buf)
    }

    /// Derive the 256-byte key expansion via the TLS-1.0 PRF and split it into
    /// per-direction keys, exactly as the Go `ovpnControl` does. Uses the
    /// server random generated in [`advance_control`] so the PRF inputs match
    /// what was sent to the client.
    fn derive_keys(&mut self, kx: &KeyExchange) -> io::Result<()> {
        let (sr1, sr2) = self.server_random.split_at(32);

        // master = PRF10(pre_master, "OpenVPN master secret", r1 || server_r1)
        let mut master = [0u8; 48];
        let mut seed = Vec::with_capacity(64);
        seed.extend_from_slice(&kx.random1);
        seed.extend_from_slice(sr1);
        let label = format!("{} master secret", KEY_EXPANSION_ID);
        prf10(&mut master, &kx.pre_master, label.as_bytes(), &seed);

        // expansion = PRF10(master, "OpenVPN key expansion",
        //                   r2 || server_r2 || peer_id || local_id)
        let mut expansion = [0u8; 256];
        let mut seed2 = Vec::with_capacity(32 + 32 + 8 + 8);
        seed2.extend_from_slice(&kx.random2);
        seed2.extend_from_slice(sr2);
        seed2.extend_from_slice(&self.reliable.peer_id);
        seed2.extend_from_slice(&self.reliable.local_id);
        let label2 = format!("{} key expansion", KEY_EXPANSION_ID);
        prf10(&mut expansion, &master, label2.as_bytes(), &seed2);

        self.keys = Some(PeerKeys::from_expansion(&expansion));
        Ok(())
    }

    // --- data channel ---------------------------------------------------------

    fn handle_data(&mut self, data: &[u8]) -> io::Result<PeerOutput> {
        let mut out = PeerOutput::default();
        let (Some(opts), Some(keys)) = (self.opts.as_ref(), self.keys.as_ref()) else {
            return Err(invalid("stream not ready for data transmission"));
        };

        let mut buf = data.to_vec();
        // `None` = auth failure; drop silently.
        if let Some(dec) = data::decrypt(opts, keys, &mut buf)? {
            if !self.replay.check(dec.pid) {
                return Ok(out); // replay — drop
            }
            if dec.is_ping {
                return Ok(out);
            }
            out.deliver = Some(dec.payload.to_vec());
        }
        Ok(out)
    }

    /// Encrypt and frame an outbound IP packet / Ethernet frame for the peer.
    pub fn send_data(&mut self, payload: &[u8]) -> io::Result<Vec<u8>> {
        let (Some(opts), Some(keys)) = (self.opts.as_ref(), self.keys.as_ref()) else {
            return Err(invalid("stream not ready for data transmission"));
        };
        self.out_pid = self.out_pid.wrapping_add(1);
        data::encrypt(opts, keys, self.out_pid, payload, fill_random)
    }
}

/// Parsed key-method 2 client exchange.
struct KeyExchange {
    pre_master: [u8; 48],
    random1: [u8; 32],
    random2: [u8; 32],
    options_server: String,
    opts: Options,
    username: String,
    password: String,
    peer_info: std::collections::HashMap<String, String>,
    peer_info_raw: String,
}

/// Read a control string at `pos`: a big-endian u16 length followed by that
/// many bytes, NUL-terminated. Returns the string (without the NUL) and the
/// new position, or `None` if the buffer doesn't yet hold the whole string.
fn read_control_string(buf: &[u8], pos: usize) -> io::Result<Option<(String, usize)>> {
    if pos + 2 > buf.len() {
        return Ok(None);
    }
    let len = u16::from_be_bytes([buf[pos], buf[pos + 1]]) as usize;
    if len == 0 {
        return Err(invalid("empty control string"));
    }
    let start = pos + 2;
    if start + len > buf.len() {
        return Ok(None);
    }
    let raw = &buf[start..start + len];
    if raw[len - 1] != 0 {
        return Err(invalid("control string not NUL-terminated"));
    }
    let s = String::from_utf8_lossy(&raw[..len - 1]).into_owned();
    Ok(Some((s, start + len)))
}

fn write_control_string(buf: &mut Vec<u8>, s: &str) {
    let len = s.len() + 1;
    buf.extend_from_slice(&(len as u16).to_be_bytes());
    buf.extend_from_slice(s.as_bytes());
    buf.push(0);
}

/// Parse the client's peer-info block: newline-separated `KEY=VALUE` pairs
/// (the `IV_*` advertisements OpenVPN clients send — `IV_VER`, `IV_PROTO`,
/// `IV_CIPHERS`, etc.). Mirrors the loop in the Go `peer-control.go`: blank
/// lines are skipped, a line without `=` is a hard error, and a value may
/// itself contain `=` (only the first one separates key from value).
fn parse_peer_info(raw: &str) -> io::Result<std::collections::HashMap<String, String>> {
    let mut peer_info = std::collections::HashMap::new();
    for line in raw.split('\n') {
        // Tolerate CRLF line endings some clients emit.
        let line = line.strip_suffix('\r').unwrap_or(line);
        if line.is_empty() {
            continue;
        }
        match line.find('=') {
            Some(i) => {
                peer_info.insert(line[..i].to_string(), line[i + 1..].to_string());
            }
            None => return Err(invalid("invalid string in peer_info")),
        }
    }
    Ok(peer_info)
}

/// Cryptographic randomness for IVs and session material.
pub(crate) fn fill_random(buf: &mut [u8]) -> io::Result<()> {
    getrandom::getrandom(buf).map_err(|e| io::Error::other(format!("getrandom: {e}")))
}

#[cfg(test)]
mod tests {
    use super::parse_peer_info;

    #[test]
    fn peer_info_parses_iv_keys() {
        let raw = "IV_VER=2.6.0\nIV_PLAT=linux\nIV_PROTO=6\nIV_CIPHERS=AES-256-GCM:AES-128-GCM\n";
        let pi = parse_peer_info(raw).unwrap();
        assert_eq!(pi.get("IV_VER").map(String::as_str), Some("2.6.0"));
        assert_eq!(pi.get("IV_PLAT").map(String::as_str), Some("linux"));
        assert_eq!(pi.get("IV_PROTO").map(String::as_str), Some("6"));
        assert_eq!(
            pi.get("IV_CIPHERS").map(String::as_str),
            Some("AES-256-GCM:AES-128-GCM")
        );
        assert_eq!(pi.len(), 4);
    }

    #[test]
    fn peer_info_value_may_contain_equals() {
        // Only the first '=' splits key from value.
        let pi = parse_peer_info("UV_OPT=a=b=c\n").unwrap();
        assert_eq!(pi.get("UV_OPT").map(String::as_str), Some("a=b=c"));
    }

    #[test]
    fn peer_info_skips_blank_and_crlf_lines() {
        let pi = parse_peer_info("\nIV_VER=2.6\r\n\n").unwrap();
        assert_eq!(pi.len(), 1);
        assert_eq!(pi.get("IV_VER").map(String::as_str), Some("2.6"));
    }

    #[test]
    fn peer_info_rejects_line_without_equals() {
        assert!(parse_peer_info("IV_VER=2.6\nGARBAGE\n").is_err());
    }

    #[test]
    fn peer_info_empty_is_ok() {
        let pi = parse_peer_info("").unwrap();
        assert!(pi.is_empty());
    }
}