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}