Skip to main content

netget/server/openvpn/
keymethod.rs

1//! OpenVPN "key method 2": the first message each side sends inside the TLS
2//! control channel.
3//!
4//! Once the TLS handshake finishes, both peers write a plaintext record into the
5//! TLS session laid out like this (`ssl.c`, `key_method_2_write` /
6//! `key_method_2_read`):
7//!
8//! ```text
9//! u32       0                        -- literal, historical
10//! u8        key method               -- 2, in the low bits
11//! [48]      pre-master secret        -- CLIENT ONLY; the server omits it
12//! [32]      random1
13//! [32]      random2
14//! string    options string           -- the sender's OCC options
15//! string    username                 -- empty when not doing user/pass auth
16//! string    password
17//! string    peer info                -- IV_* variables; empty from a server
18//! ```
19//!
20//! A `string` is a `u16` length followed by that many bytes, and the length
21//! **includes a trailing NUL**, which is part of the payload. `write_empty_string`
22//! emits a length of zero and no bytes at all, which is what a server sends for
23//! username, password and peer info — so "empty" and "one NUL byte" are two
24//! different encodings and only the first is what the peer expects from us.
25//!
26//! # What this module does and does not do
27//!
28//! It parses the client's message and builds the server's answer. It does **not**
29//! derive data-channel keys from the exchanged material: that needs the TLS
30//! PRF over the combined key sources, and the data channel it would key does
31//! not exist. `pre_master` is deliberately never copied out of the parsed
32//! struct into anything persistent.
33
34use anyhow::{bail, Result};
35
36/// The only key method this server understands.
37pub const KEY_METHOD_2: u8 = 2;
38
39/// `key_source2` field sizes, from `ssl_common.h`.
40const PRE_MASTER_LEN: usize = 48;
41const RANDOM_LEN: usize = 32;
42
43/// Refuse absurd string lengths early. OpenVPN's own cap on the options string
44/// is `TLS_OPTIONS_LEN` (512); usernames and peer info are smaller still, and
45/// the whole message arrives inside one TLS record.
46const MAX_STRING_LEN: usize = 4096;
47
48/// The client's key-method-2 message, as parsed off the control channel.
49#[derive(Debug, Clone)]
50pub struct ClientKeyMethod2 {
51    /// Client entropy. Half of the data-channel key material, and a secret:
52    /// never logged, never put in event data.
53    pub pre_master: Vec<u8>,
54    pub random1: Vec<u8>,
55    pub random2: Vec<u8>,
56    /// The client's OCC options string, e.g.
57    /// `V4,dev-type tun,link-mtu 1549,...,tls-client`.
58    pub options: String,
59    /// `--auth-user-pass` username, or empty.
60    pub username: String,
61    /// `--auth-user-pass` password, or empty.
62    pub password: String,
63    /// `IV_*` variables the client advertises: version, platform, protocol
64    /// flags, supported data ciphers.
65    pub peer_info: String,
66}
67
68/// Read a `u16`-prefixed, NUL-terminated string.
69///
70/// Returns `Ok(None)` when the buffer does not yet hold the whole string, which
71/// is not an error: the control channel is a stream and the rest may still be in
72/// flight.
73fn read_string(buf: &[u8], pos: &mut usize) -> Result<Option<String>> {
74    if buf.len() < *pos + 2 {
75        return Ok(None);
76    }
77    let len = u16::from_be_bytes([buf[*pos], buf[*pos + 1]]) as usize;
78    if len > MAX_STRING_LEN {
79        bail!("control-channel string length {} is implausible", len);
80    }
81    if buf.len() < *pos + 2 + len {
82        return Ok(None);
83    }
84    let start = *pos + 2;
85    let bytes = &buf[start..start + len];
86    *pos = start + len;
87    // The length counts the NUL terminator; strip it rather than carrying it
88    // into a Rust string.
89    let bytes = match bytes.last() {
90        Some(0) => &bytes[..bytes.len() - 1],
91        _ => bytes,
92    };
93    Ok(Some(String::from_utf8_lossy(bytes).into_owned()))
94}
95
96fn write_string(out: &mut Vec<u8>, s: &str) {
97    let len = s.len() + 1;
98    out.extend_from_slice(&(len as u16).to_be_bytes());
99    out.extend_from_slice(s.as_bytes());
100    out.push(0);
101}
102
103/// `write_empty_string`: a zero length and no bytes. Not the same as a string
104/// containing only a NUL.
105fn write_empty_string(out: &mut Vec<u8>) {
106    out.extend_from_slice(&0u16.to_be_bytes());
107}
108
109/// Parse a client key-method-2 message.
110///
111/// * `Ok(Some((msg, consumed)))` — a whole message was read.
112/// * `Ok(None)` — the buffer is a prefix of one; wait for more control packets.
113/// * `Err(_)` — the bytes cannot be a key-method-2 message at all.
114pub fn parse_client_key_method_2(buf: &[u8]) -> Result<Option<(ClientKeyMethod2, usize)>> {
115    let fixed = 4 + 1 + PRE_MASTER_LEN + RANDOM_LEN + RANDOM_LEN;
116    if buf.len() < fixed {
117        return Ok(None);
118    }
119
120    // The leading u32 is a literal zero in every version that speaks key
121    // method 2. Anything else means we are not looking at one.
122    let leading = u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]);
123    if leading != 0 {
124        bail!(
125            "control channel does not start with a key-method-2 message (leading u32 is {:#010x})",
126            leading
127        );
128    }
129
130    // OpenVPN masks off the high bits, which carry flags rather than the method.
131    let key_method = buf[4] & 0x07;
132    if key_method != KEY_METHOD_2 {
133        bail!(
134            "unsupported OpenVPN key method {} (only key method 2 exists in any supported release)",
135            key_method
136        );
137    }
138
139    let mut pos = 5;
140    let pre_master = buf[pos..pos + PRE_MASTER_LEN].to_vec();
141    pos += PRE_MASTER_LEN;
142    let random1 = buf[pos..pos + RANDOM_LEN].to_vec();
143    pos += RANDOM_LEN;
144    let random2 = buf[pos..pos + RANDOM_LEN].to_vec();
145    pos += RANDOM_LEN;
146
147    // All four strings are always written, so a missing one means the message is
148    // still arriving rather than that the client omitted it.
149    let options = match read_string(buf, &mut pos)? {
150        Some(s) => s,
151        None => return Ok(None),
152    };
153    let username = match read_string(buf, &mut pos)? {
154        Some(s) => s,
155        None => return Ok(None),
156    };
157    let password = match read_string(buf, &mut pos)? {
158        Some(s) => s,
159        None => return Ok(None),
160    };
161    let peer_info = match read_string(buf, &mut pos)? {
162        Some(s) => s,
163        None => return Ok(None),
164    };
165
166    Ok(Some((
167        ClientKeyMethod2 {
168            pre_master,
169            random1,
170            random2,
171            options,
172            username,
173            password,
174            peer_info,
175        },
176        pos,
177    )))
178}
179
180/// Build the server's key-method-2 answer.
181///
182/// The server sends `random1` and `random2` but **no pre-master secret** —
183/// `key_source2_randomize_write` writes one only when it is the client — and
184/// then an empty username, password and peer info, exactly as a real server
185/// with no `--auth-user-pass-verify` does.
186pub fn build_server_key_method_2(random1: &[u8; 32], random2: &[u8; 32], options: &str) -> Vec<u8> {
187    let mut out = Vec::with_capacity(128 + options.len());
188    out.extend_from_slice(&0u32.to_be_bytes());
189    out.push(KEY_METHOD_2);
190    out.extend_from_slice(random1);
191    out.extend_from_slice(random2);
192    write_string(&mut out, options);
193    write_empty_string(&mut out); // username
194    write_empty_string(&mut out); // password
195    write_empty_string(&mut out); // peer info
196    out
197}
198
199/// Derive the options string to answer with from the one the client sent.
200///
201/// The peer compares the two and warns about every field that differs, so
202/// mirroring the client's string and flipping only `tls-client` to `tls-server`
203/// produces a clean log. A client that sent nothing usable gets a minimal but
204/// well-formed V4 string instead of an empty one, which OpenVPN treats as a
205/// protocol error rather than as "no opinion".
206pub fn server_options_from_client(client_options: &str) -> String {
207    const FALLBACK: &str = "V4,dev-type tun,link-mtu 1549,tun-mtu 1500,proto UDPv4,\
208                            cipher AES-256-GCM,auth SHA1,keysize 256,key-method 2,tls-server";
209
210    if client_options.is_empty() {
211        return FALLBACK.to_string();
212    }
213    match client_options.strip_suffix("tls-client") {
214        Some(head) => format!("{}tls-server", head),
215        None => client_options.to_string(),
216    }
217}
218
219/// Peer-info lines (`IV_VER=...`) as a JSON-friendly map, for event data.
220pub fn peer_info_map(peer_info: &str) -> serde_json::Map<String, serde_json::Value> {
221    let mut map = serde_json::Map::new();
222    for line in peer_info.split(['\n', '\0']) {
223        let line = line.trim();
224        if line.is_empty() {
225            continue;
226        }
227        match line.split_once('=') {
228            Some((k, v)) => {
229                map.insert(k.to_string(), serde_json::Value::String(v.to_string()));
230            }
231            None => {
232                map.insert(line.to_string(), serde_json::Value::Bool(true));
233            }
234        }
235    }
236    map
237}