[Openvpn-devel] [L] Change in openvpn[master]: oob: Add control message TLV encoding (P_CONTROL_OOB_V1)
"stipa \(Code Review\) via Openvpn-devel" <[email protected]> Tue, 28 Jul 2026 15:03:23 +0000
| Newsgroups | net.sourceforge.lists.openvpn-devel |
|---|---|
| Message-ID | <d8237ce85e734f5c60426e411f92b3ceba65ef3b-EmailReplacePatchSet-HTML@gerrit.openvpn.net> |
Attention is currently required from: flichtenheld, stipa.
Hello flichtenheld, plaisthos,
I'd like you to reexamine a change. Please visit
http://gerrit.openvpn.net/c/openvpn/+/1741?usp=email
to look at the new patch set (#9).
The following approvals got outdated and were removed:
Code-Review+2 by flichtenheld
Change subject: oob: Add control message TLV encoding (P_CONTROL_OOB_V1)
......................................................................
oob: Add control message TLV encoding (P_CONTROL_OOB_V1)
Add opcode 12 for out-of-band control messages and the TLV codec for the
SERVER_PROBE and PROBE_REPLY used by server latency checks.
The framing is not OOB-specific, so it goes into its own control_msg.c/h;
oob.c keeps the OOB message and TLV types.
P_LAST_OPCODE becomes 12, and opcode_valid_in_session() replaces the plain
range check because OOB opcodes are answered statelessly and are never legal
on an established session.
Change-Id: I1c8d302ac57c5603d622a7be14be369437388268
Signed-off-by: Lev Stipakov <[email protected]>
---
M CMakeLists.txt
M src/openvpn/Makefile.am
A src/openvpn/control_msg.c
A src/openvpn/control_msg.h
A src/openvpn/oob.c
A src/openvpn/oob.h
M src/openvpn/ssl.c
M src/openvpn/ssl_pkt.c
M src/openvpn/ssl_pkt.h
M tests/unit_tests/openvpn/Makefile.am
A tests/unit_tests/openvpn/test_oob.c
11 files changed, 739 insertions(+), 4 deletions(-)
git pull ssh://gerrit.openvpn.net:29418/openvpn refs/changes/41/1741/9
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 7473f15..f4b1b22 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -444,6 +444,8 @@
src/openvpn/console.c
src/openvpn/console_builtin.c
src/openvpn/console.h
+ src/openvpn/control_msg.c
+ src/openvpn/control_msg.h
src/openvpn/crypto.c
src/openvpn/crypto.h
src/openvpn/crypto_backend.h
@@ -520,6 +522,8 @@
src/openvpn/multi_io.c
src/openvpn/occ.c
src/openvpn/occ.h
+ src/openvpn/oob.c
+ src/openvpn/oob.h
src/openvpn/openvpn.c
src/openvpn/openvpn.h
src/openvpn/openvpn_win32_resources.rc
@@ -667,6 +671,7 @@
"test_mbuf"
"test_misc"
"test_ncp"
+ "test_oob"
"test_options_parse"
"test_packet_id"
"test_pkt"
@@ -858,6 +863,13 @@
src/openvpn/session_id.c
)
+ target_sources(test_oob PRIVATE
+ tests/unit_tests/openvpn/mock_get_random.c
+ src/openvpn/control_msg.c
+ src/openvpn/oob.c
+ src/openvpn/session_id.c
+ )
+
target_sources(test_pkt PRIVATE
tests/unit_tests/openvpn/mock_win32_execve.c
src/openvpn/argv.c
diff --git a/src/openvpn/Makefile.am b/src/openvpn/Makefile.am
index ff8cc54..a67d478 100644
--- a/src/openvpn/Makefile.am
+++ b/src/openvpn/Makefile.am
@@ -52,6 +52,7 @@
common.h \
comp.c comp.h compstub.c \
comp-lz4.c comp-lz4.h \
+ control_msg.c control_msg.h \
crypto.c crypto.h crypto_backend.h \
crypto_openssl.c crypto_openssl.h \
crypto_mbedtls_legacy.c crypto_mbedtls_legacy.h \
@@ -106,6 +107,7 @@
pkcs11.c pkcs11.h pkcs11_backend.h \
pkcs11_openssl.c \
pkcs11_mbedtls.c \
+ oob.c oob.h \
openvpn.c openvpn.h \
options.c options.h \
options_util.c options_util.h \
diff --git a/src/openvpn/control_msg.c b/src/openvpn/control_msg.c
new file mode 100644
index 0000000..95ff210
--- /dev/null
+++ b/src/openvpn/control_msg.c
@@ -0,0 +1,90 @@
+/*
+ * OpenVPN -- An application to securely tunnel IP networks
+ * over a single TCP/UDP port, with support for SSL/TLS-based
+ * session authentication and key exchange,
+ * packet encryption, packet authentication, and
+ * packet compression.
+ *
+ * Copyright (C) 2002-2026 OpenVPN Inc <[email protected]>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "syshead.h"
+
+#include "control_msg.h"
+
+bool
+ctrl_msg_read_header(struct buffer *buf, uint16_t expected_msg_type)
+{
+ int msg_type = buf_read_u16(buf);
+ return msg_type >= 0 && (uint16_t)msg_type == expected_msg_type;
+}
+
+bool
+ctrl_msg_tlv_write_header(struct buffer *buf, uint16_t type, bool optional, uint16_t value_len)
+{
+ uint16_t field = type & CTRL_MSG_TLV_TYPE_MASK;
+ if (optional)
+ {
+ field |= CTRL_MSG_TLV_OPTIONAL_FLAG;
+ }
+ return buf_write_u16(buf, field) && buf_write_u16(buf, value_len);
+}
+
+bool
+ctrl_msg_tlv_read_header(struct buffer *buf, struct ctrl_msg_tlv_header *hdr)
+{
+ int field = buf_read_u16(buf);
+ if (field < 0)
+ {
+ return false;
+ }
+ int len = buf_read_u16(buf);
+ if (len < 0)
+ {
+ return false;
+ }
+ hdr->type = (uint16_t)(field & CTRL_MSG_TLV_TYPE_MASK);
+ hdr->optional = (field & CTRL_MSG_TLV_OPTIONAL_FLAG) != 0;
+ hdr->value_len = (uint16_t)len;
+ return true;
+}
+
+bool
+ctrl_msg_find_tlv(struct buffer *payload, uint16_t wanted_type, struct buffer *value)
+{
+ struct ctrl_msg_tlv_header hdr;
+ while (ctrl_msg_tlv_read_header(payload, &hdr))
+ {
+ /* Take the value out of payload. This validates that the header's
+ * length is really there and advances past it in one step, so the TLV
+ * we are looking for and the ones we skip are bounds-checked alike. */
+ uint8_t *v = buf_read_alloc(payload, hdr.value_len);
+ if (!v)
+ {
+ return false;
+ }
+ if (hdr.type == wanted_type)
+ {
+ buf_set_read(value, v, hdr.value_len);
+ return true;
+ }
+ /* not the TLV we want: keep scanning */
+ }
+ return false;
+}
diff --git a/src/openvpn/control_msg.h b/src/openvpn/control_msg.h
new file mode 100644
index 0000000..e4c8b54
--- /dev/null
+++ b/src/openvpn/control_msg.h
@@ -0,0 +1,101 @@
+/*
+ * OpenVPN -- An application to securely tunnel IP networks
+ * over a single TCP/UDP port, with support for SSL/TLS-based
+ * session authentication and key exchange,
+ * packet encryption, packet authentication, and
+ * packet compression.
+ *
+ * Copyright (C) 2002-2026 OpenVPN Inc <[email protected]>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+/**
+ * @file
+ * Framing shared by the TLV-based control messages of the wire protocol.
+ *
+ * Such a message payload starts with a 16-bit message type, followed by a
+ * sequence of TLV entries. Each TLV starts with a 4-byte header: a 16-bit field
+ * whose most significant bit is the "optional" flag and whose remaining 15 bits
+ * are the type, followed by a 16-bit length giving the size of the value that
+ * follows the header.
+ *
+ * The message-type and TLV-type values themselves are specific to the message
+ * family carried (out-of-band messages define theirs in oob.h), so only the
+ * framing lives here.
+ */
+
+#ifndef CONTROL_MSG_H
+#define CONTROL_MSG_H
+
+#include "buffer.h"
+
+/* TLV header bit layout of the first 16-bit field */
+#define CTRL_MSG_TLV_OPTIONAL_FLAG 0x8000
+#define CTRL_MSG_TLV_TYPE_MASK 0x7fff
+
+/* The header every TLV carries: the 15-bit type and optional flag packed into
+ * the first 16-bit field, then the length of the value that follows. */
+struct ctrl_msg_tlv_header
+{
+ uint16_t type; /**< the 15-bit TLV type */
+ bool optional; /**< value of the optional flag */
+ uint16_t value_len; /**< length of the value following the header */
+};
+
+/**
+ * Read and verify a message-type header from buf, advancing past it.
+ *
+ * @param buf buffer positioned at the message payload
+ * @param expected_msg_type the message type the payload must carry
+ * @return true if a message type was read and equals expected_msg_type,
+ * false on a short buffer or a mismatching type.
+ */
+bool ctrl_msg_read_header(struct buffer *buf, uint16_t expected_msg_type);
+
+/**
+ * Write a TLV header (type + optional flag + value length) to buf.
+ *
+ * @return true on success, false if buf has insufficient space.
+ */
+bool ctrl_msg_tlv_write_header(struct buffer *buf, uint16_t type, bool optional,
+ uint16_t value_len);
+
+/**
+ * Read a TLV header from buf, advancing past it.
+ *
+ * @param buf buffer positioned at the TLV header
+ * @param hdr filled with the type, optional flag and value length on success
+ * @return true on success, false if there are not enough bytes for a header.
+ */
+bool ctrl_msg_tlv_read_header(struct buffer *buf, struct ctrl_msg_tlv_header *hdr);
+
+/**
+ * Scan payload for the first TLV of type wanted_type, skipping any other (e.g.
+ * future) TLV types.
+ *
+ * On success value covers exactly the found TLV's value bytes. The length from
+ * each TLV header is validated against payload as the scan goes, so the whole
+ * value is guaranteed to be present; a header claiming more bytes than payload
+ * holds is rejected rather than reported as found. payload is consumed as it is
+ * read.
+ *
+ * @param payload buffer positioned at a TLV header
+ * @param wanted_type the TLV type to look for
+ * @param value set to a buffer covering the found TLV's value
+ * @return true if the TLV was found, false if it is not present or a TLV
+ * header or value is malformed or truncated.
+ */
+bool ctrl_msg_find_tlv(struct buffer *payload, uint16_t wanted_type, struct buffer *value);
+
+#endif /* ifndef CONTROL_MSG_H */
diff --git a/src/openvpn/oob.c b/src/openvpn/oob.c
new file mode 100644
index 0000000..c807171
--- /dev/null
+++ b/src/openvpn/oob.c
@@ -0,0 +1,83 @@
+/*
+ * OpenVPN -- An application to securely tunnel IP networks
+ * over a single TCP/UDP port, with support for SSL/TLS-based
+ * session authentication and key exchange,
+ * packet encryption, packet authentication, and
+ * packet compression.
+ *
+ * Copyright (C) 2002-2026 OpenVPN Inc <[email protected]>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "syshead.h"
+
+#include "oob.h"
+#include "control_msg.h"
+
+bool
+oob_probe_parameter_write(struct buffer *buf, const struct oob_probe_parameter *p)
+{
+ return ctrl_msg_tlv_write_header(buf, OOB_TLV_PROBE_PARAMETER, false, OOB_PROBE_PARAMETER_LEN)
+ && buf_write_u64(buf, p->timestamp)
+ && buf_write_u32(buf, p->flags);
+}
+
+bool
+oob_probe_parameter_read(struct buffer *buf, struct oob_probe_parameter *p)
+{
+ /* One bounds check covers the whole value: every field read below then fits
+ * by construction (OOB_PROBE_PARAMETER_LEN is the sum of their sizes), so
+ * none of them needs its own error handling. Trailing bytes this version
+ * does not understand are simply left unread. */
+ if (buf_len(buf) < OOB_PROBE_PARAMETER_LEN)
+ {
+ return false;
+ }
+ p->timestamp = buf_read_u64(buf, NULL);
+ p->flags = buf_read_u32(buf, NULL);
+ return true;
+}
+
+bool
+oob_probe_reply_write(struct buffer *buf, const struct oob_probe_reply *r)
+{
+ return ctrl_msg_tlv_write_header(buf, OOB_TLV_PROBE_REPLY, false, OOB_PROBE_REPLY_LEN)
+ && session_id_write(&r->peer_session_id, buf)
+ && buf_write_u16(buf, r->priority)
+ && buf_write_u16(buf, r->weight)
+ && buf_write_u16(buf, r->max_latency_diff)
+ && buf_write_u16(buf, r->connect_lifetime)
+ && buf_write_u32(buf, r->flags);
+}
+
+bool
+oob_probe_reply_read(struct buffer *buf, struct oob_probe_reply *r)
+{
+ /* One bounds check for the whole value, as in oob_probe_parameter_read(). */
+ if (buf_len(buf) < OOB_PROBE_REPLY_LEN)
+ {
+ return false;
+ }
+ session_id_read(&r->peer_session_id, buf);
+ r->priority = (uint16_t)buf_read_u16(buf);
+ r->weight = (uint16_t)buf_read_u16(buf);
+ r->max_latency_diff = (uint16_t)buf_read_u16(buf);
+ r->connect_lifetime = (uint16_t)buf_read_u16(buf);
+ r->flags = buf_read_u32(buf, NULL);
+ return true;
+}
diff --git a/src/openvpn/oob.h b/src/openvpn/oob.h
new file mode 100644
index 0000000..0a9a4bb
--- /dev/null
+++ b/src/openvpn/oob.h
@@ -0,0 +1,110 @@
+/*
+ * OpenVPN -- An application to securely tunnel IP networks
+ * over a single TCP/UDP port, with support for SSL/TLS-based
+ * session authentication and key exchange,
+ * packet encryption, packet authentication, and
+ * packet compression.
+ *
+ * Copyright (C) 2002-2026 OpenVPN Inc <[email protected]>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+/**
+ * @file
+ * Encoding/decoding of out-of-band (P_CONTROL_OOB_V1) control messages.
+ *
+ * An OOB message payload starts with a 16-bit message type (the 0x1xx space:
+ * SERVER_PROBE, PROBE_REPLY, ...) followed by a sequence of TLV entries in the
+ * 0x2xx type space. The framing itself is shared with the other TLV-based
+ * control messages of the wire protocol and lives in control_msg.h; this file
+ * defines the OOB message and TLV types, their values, and the OOB-specific
+ * decisions taken on them.
+ */
+
+#ifndef OOB_H
+#define OOB_H
+
+#include "buffer.h"
+#include "session_id.h"
+
+/* OOB message types: the 16-bit value at the start of an OOB payload, before
+ * its TLV entries. Distinct from the TLV-type space (0x2xx); see the "Messages"
+ * table in the OOB section of the wire protocol spec. */
+#define OOB_MSG_SERVER_PROBE 0x100
+#define OOB_MSG_PROBE_REPLY 0x101
+
+/* TLV types (see the OOB control message section of the wire protocol spec) */
+#define OOB_TLV_PROBE_PARAMETER 0x200
+#define OOB_TLV_PROBE_REPLY 0x201
+
+/* Minimum on-wire value length (excluding the 4-byte TLV header) of each TLV:
+ * the sizes of its fixed fields in wire order (see the structs below). The
+ * value may be longer for forward compatibility; trailing bytes that are not
+ * understood are ignored on read. */
+#define OOB_PROBE_PARAMETER_LEN \
+ ((uint16_t)(sizeof(uint64_t) /* timestamp */ + sizeof(uint32_t) /* flags */))
+#define OOB_PROBE_REPLY_LEN \
+ ((uint16_t)(SID_SIZE /* peer_session_id */ \
+ + 4 * sizeof(uint16_t) /* priority, weight, \
+ * max_latency_diff, \
+ * connect_lifetime */ \
+ + sizeof(uint32_t))) /* flags */
+
+/* probe parameter TLV (sent by the client in a SERVER_PROBE) */
+struct oob_probe_parameter
+{
+ uint64_t timestamp; /**< client clock as a UNIX timestamp */
+ uint32_t flags; /**< client capability flags, currently must be 0 */
+};
+
+/* probe reply TLV (sent by the server in a PROBE_REPLY) */
+struct oob_probe_reply
+{
+ struct session_id peer_session_id; /**< echoes the session id of the request */
+ uint16_t priority; /**< DNS-SRV style priority (lower is preferred) */
+ uint16_t weight; /**< DNS-SRV style weight */
+ uint16_t max_latency_diff; /**< advertised candidate-band margin in ms;
+ * 0 means "defer to the client's setting" */
+ uint16_t connect_lifetime; /**< seconds the reply stays valid as the handshake reset */
+ uint32_t flags; /**< server behaviour flags */
+};
+
+/**
+ * Write a complete probe parameter TLV (header + value) to buf.
+ */
+bool oob_probe_parameter_write(struct buffer *buf, const struct oob_probe_parameter *p);
+
+/**
+ * Read a probe parameter TLV value from buf.
+ *
+ * buf must cover exactly the TLV's value, as returned by ctrl_msg_find_tlv().
+ * Trailing bytes beyond the fields understood here are ignored, so a longer
+ * value from a future version still parses.
+ *
+ * @return true on success, false if buf is shorter than the mandatory fields.
+ */
+bool oob_probe_parameter_read(struct buffer *buf, struct oob_probe_parameter *p);
+
+/**
+ * Write a complete probe reply TLV (header + value) to buf.
+ */
+bool oob_probe_reply_write(struct buffer *buf, const struct oob_probe_reply *r);
+
+/**
+ * Read a probe reply TLV value from buf. See oob_probe_parameter_read() for the
+ * calling convention.
+ */
+bool oob_probe_reply_read(struct buffer *buf, struct oob_probe_reply *r);
+
+#endif /* OOB_H */
diff --git a/src/openvpn/ssl.c b/src/openvpn/ssl.c
index 60df7ce..9e1a6aa 100644
--- a/src/openvpn/ssl.c
+++ b/src/openvpn/ssl.c
@@ -3616,7 +3616,7 @@
struct session_id sid; /* remote session ID */
/* verify legal opcode */
- if (op < P_FIRST_OPCODE || op > P_LAST_OPCODE)
+ if (!opcode_valid_in_session(op))
{
if (op == P_CONTROL_HARD_RESET_CLIENT_V1 || op == P_CONTROL_HARD_RESET_SERVER_V1)
{
diff --git a/src/openvpn/ssl_pkt.c b/src/openvpn/ssl_pkt.c
index f8444451..0d882d9 100644
--- a/src/openvpn/ssl_pkt.c
+++ b/src/openvpn/ssl_pkt.c
@@ -166,7 +166,7 @@
bool prepend_ack)
{
ASSERT(ks->key_id >= 0 && ks->key_id <= P_KEY_ID_MASK);
- ASSERT(opcode >= 0 && opcode <= P_LAST_OPCODE);
+ ASSERT(opcode_valid_in_session(opcode));
uint8_t header = (uint8_t)(ks->key_id | (opcode << P_OPCODE_SHIFT));
/* Workaround for Softether servers. Softether has a bug that it only
diff --git a/src/openvpn/ssl_pkt.h b/src/openvpn/ssl_pkt.h
index 82cb5b1..25e50bf 100644
--- a/src/openvpn/ssl_pkt.h
+++ b/src/openvpn/ssl_pkt.h
@@ -58,11 +58,53 @@
* like P_CONTROL_HARD_RESET_CLIENT_V3 */
#define P_CONTROL_WKC_V1 11
-/* define the range of legal opcodes
+/* Out-of-band control message that does not belong to an established
+ * control channel session (e.g. a server probe). Inherently unreliable:
+ * there is no protocol-level retransmission.
+ *
+ * It is not legal on an established session; see opcode_valid_in_session()
+ * below. */
+#define P_CONTROL_OOB_V1 12
+
+/* define the range of defined opcodes, in- and out-of-band. Note this is not
+ * the set of opcodes legal on an established session; see
+ * opcode_valid_in_session().
* Since we do no longer support key-method 1 we consider
* the v1 op codes invalid */
#define P_FIRST_OPCODE 3
-#define P_LAST_OPCODE 11
+#define P_LAST_OPCODE 12
+
+static inline bool
+opcode_is_oob(int op)
+{
+ return op == P_CONTROL_OOB_V1;
+}
+
+/**
+ * Return true if op may occur on an established control-channel session.
+ *
+ * Out-of-band opcodes may not. They are answered statelessly on the
+ * new-connection path, and being rejected here is a permanent property rather
+ * than a handler that is still missing:
+ *
+ * - an OOB message carries its TLV payload directly, with no reliability or
+ * ACK fields (see tls_wrap_oob_standalone()), whereas the established-session
+ * path parses an ACK array and a control packet-id before anything else. The
+ * payload is chosen by the sender and session ids are plaintext on the wire,
+ * so those bytes can be crafted into a valid ACK array, forging ACKs into a
+ * live control channel and stalling a handshake or rekey.
+ * - nothing on that path parses an OOB payload, so there is nothing to gain by
+ * accepting one.
+ *
+ * This cannot be expressed as an opcode range: the inband CONTROL_DATA_V1 of the
+ * wire protocol is legal on an established session, so the OOB opcodes sit
+ * between legal ones.
+ */
+static inline bool
+opcode_valid_in_session(int op)
+{
+ return op >= P_FIRST_OPCODE && op <= P_LAST_OPCODE && !opcode_is_oob(op);
+}
/*
* Define number of buffers for send and receive in the reliability layer.
@@ -264,6 +306,9 @@
case P_CONTROL_WKC_V1:
return "P_CONTROL_WKC_V1";
+ case P_CONTROL_OOB_V1:
+ return "P_CONTROL_OOB_V1";
+
case P_ACK_V1:
return "P_ACK_V1";
diff --git a/tests/unit_tests/openvpn/Makefile.am b/tests/unit_tests/openvpn/Makefile.am
index d861ef9..611097a 100644
--- a/tests/unit_tests/openvpn/Makefile.am
+++ b/tests/unit_tests/openvpn/Makefile.am
@@ -14,6 +14,7 @@
ncp_testdriver \
mbuf_testdriver \
misc_testdriver \
+ oob_testdriver \
options_parse_testdriver \
packet_id_testdriver \
pkt_testdriver \
@@ -150,6 +151,20 @@
$(top_srcdir)/src/openvpn/win32-util.c \
$(top_srcdir)/src/openvpn/session_id.c
+oob_testdriver_CFLAGS = \
+ -I$(top_srcdir)/include -I$(top_srcdir)/src/compat -I$(top_srcdir)/src/openvpn \
+ @TEST_CFLAGS@
+oob_testdriver_LDFLAGS = @TEST_LDFLAGS@
+oob_testdriver_SOURCES = test_oob.c \
+ mock_msg.c mock_msg.h test_common.h \
+ mock_get_random.c \
+ $(top_srcdir)/src/openvpn/buffer.c \
+ $(top_srcdir)/src/openvpn/control_msg.c \
+ $(top_srcdir)/src/openvpn/oob.c \
+ $(top_srcdir)/src/openvpn/platform.c \
+ $(top_srcdir)/src/openvpn/session_id.c \
+ $(top_srcdir)/src/openvpn/win32-util.c
+
pkt_testdriver_CFLAGS = \
-I$(top_srcdir)/include -I$(top_srcdir)/src/compat -I$(top_srcdir)/src/openvpn \
@TEST_CFLAGS@
diff --git a/tests/unit_tests/openvpn/test_oob.c b/tests/unit_tests/openvpn/test_oob.c
new file mode 100644
index 0000000..aa4eb23
--- /dev/null
+++ b/tests/unit_tests/openvpn/test_oob.c
@@ -0,0 +1,277 @@
+/*
+ * OpenVPN -- An application to securely tunnel IP networks
+ * over a single TCP/UDP port, with support for SSL/TLS-based
+ * session authentication and key exchange,
+ * packet encryption, packet authentication, and
+ * packet compression.
+ *
+ * Copyright (C) 2002-2026 OpenVPN Inc <[email protected]>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "syshead.h"
+
+#include <stdarg.h>
+#include <stddef.h>
+#include <setjmp.h>
+#include <cmocka.h>
+
+#include "control_msg.h"
+#include "oob.h"
+#include "test_common.h"
+
+/* Write a probe parameter TLV and read it back; fields must survive the
+ * round trip and the whole buffer must be consumed. */
+static void
+test_probe_parameter_roundtrip(void **state)
+{
+ struct gc_arena gc = gc_new();
+ struct buffer buf = alloc_buf_gc(128, &gc);
+
+ const struct oob_probe_parameter in = {
+ .timestamp = 0x0123456789abcdefULL,
+ .flags = 0,
+ };
+ assert_true(oob_probe_parameter_write(&buf, &in));
+ /* header (4) + value (12) */
+ assert_int_equal(BLEN(&buf), 4 + OOB_PROBE_PARAMETER_LEN);
+
+ /* the header codec, read from a copy so the scan below still sees it */
+ struct buffer peek = buf;
+ struct ctrl_msg_tlv_header hdr;
+ assert_true(ctrl_msg_tlv_read_header(&peek, &hdr));
+ assert_int_equal(hdr.type, OOB_TLV_PROBE_PARAMETER);
+ assert_false(hdr.optional);
+ assert_int_equal(hdr.value_len, OOB_PROBE_PARAMETER_LEN);
+
+ struct buffer value;
+ assert_true(ctrl_msg_find_tlv(&buf, OOB_TLV_PROBE_PARAMETER, &value));
+ assert_int_equal(BLEN(&value), OOB_PROBE_PARAMETER_LEN);
+
+ struct oob_probe_parameter out = { 0 };
+ assert_true(oob_probe_parameter_read(&value, &out));
+ assert_true(in.timestamp == out.timestamp);
+ assert_int_equal(in.flags, out.flags);
+ /* the scan consumed header and value alike */
+ assert_int_equal(BLEN(&buf), 0);
+
+ gc_free(&gc);
+}
+
+/* Write a probe reply TLV and read it back. */
+static void
+test_probe_reply_roundtrip(void **state)
+{
+ struct gc_arena gc = gc_new();
+ struct buffer buf = alloc_buf_gc(128, &gc);
+
+ struct oob_probe_reply in = {
+ .priority = 10,
+ .weight = 100,
+ .connect_lifetime = 30,
+ .flags = 1,
+ .max_latency_diff = 25,
+ };
+ memcpy(in.peer_session_id.id, "ABCDEFGH", SID_SIZE);
+
+ assert_true(oob_probe_reply_write(&buf, &in));
+ assert_int_equal(BLEN(&buf), 4 + OOB_PROBE_REPLY_LEN);
+
+ struct buffer peek = buf;
+ struct ctrl_msg_tlv_header hdr;
+ assert_true(ctrl_msg_tlv_read_header(&peek, &hdr));
+ assert_int_equal(hdr.type, OOB_TLV_PROBE_REPLY);
+ assert_int_equal(hdr.value_len, OOB_PROBE_REPLY_LEN);
+
+ struct buffer value;
+ assert_true(ctrl_msg_find_tlv(&buf, OOB_TLV_PROBE_REPLY, &value));
+
+ struct oob_probe_reply out = { 0 };
+ assert_true(oob_probe_reply_read(&value, &out));
+ assert_memory_equal(in.peer_session_id.id, out.peer_session_id.id, SID_SIZE);
+ assert_int_equal(in.priority, out.priority);
+ assert_int_equal(in.weight, out.weight);
+ assert_int_equal(in.connect_lifetime, out.connect_lifetime);
+ assert_int_equal(in.flags, out.flags);
+ assert_int_equal(in.max_latency_diff, out.max_latency_diff);
+ assert_int_equal(BLEN(&buf), 0);
+
+ gc_free(&gc);
+}
+
+/* The probe reply wire format is locked to the spec's field order
+ * (priority, weight, max_latency_diff, connect_lifetime, flags), big-endian. */
+static void
+test_probe_reply_wire_format(void **state)
+{
+ struct gc_arena gc = gc_new();
+ struct buffer buf = alloc_buf_gc(128, &gc);
+
+ struct oob_probe_reply in = {
+ .priority = 10,
+ .weight = 100,
+ .connect_lifetime = 30,
+ .flags = 1,
+ .max_latency_diff = 25,
+ };
+ memcpy(in.peer_session_id.id, "ABCDEFGH", SID_SIZE);
+
+ assert_true(oob_probe_reply_write(&buf, &in));
+
+ const uint8_t expected[] = {
+ 0x02,
+ 0x01, /* TLV type 0x201 (not optional) */
+ 0x00,
+ 0x14, /* TLV value length = 20 */
+ 'A',
+ 'B',
+ 'C',
+ 'D',
+ 'E',
+ 'F',
+ 'G',
+ 'H', /* peer_session_id */
+ 0x00,
+ 0x0a, /* priority = 10 */
+ 0x00,
+ 0x64, /* weight = 100 */
+ 0x00,
+ 0x19, /* max_latency_diff = 25 */
+ 0x00,
+ 0x1e, /* connect_lifetime = 30 */
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x01, /* flags = 1 */
+ };
+ assert_int_equal(BLEN(&buf), sizeof(expected));
+ assert_memory_equal(BPTR(&buf), expected, sizeof(expected));
+
+ gc_free(&gc);
+}
+
+/* A TLV with a longer-than-known value must still parse: the known fields are
+ * read and the trailing bytes are skipped (forward compatibility). */
+static void
+test_probe_parameter_forward_compat(void **state)
+{
+ struct gc_arena gc = gc_new();
+ struct buffer buf = alloc_buf_gc(128, &gc);
+
+ const uint16_t extended_len = OOB_PROBE_PARAMETER_LEN + 4;
+ assert_true(ctrl_msg_tlv_write_header(&buf, OOB_TLV_PROBE_PARAMETER, false, extended_len));
+ assert_true(buf_write_u32(&buf, 0)); /* timestamp high */
+ assert_true(buf_write_u32(&buf, 0xdeadbeef)); /* timestamp low */
+ assert_true(buf_write_u32(&buf, 0)); /* flags */
+ assert_true(buf_write_u32(&buf, 0x11223344)); /* unknown trailing field */
+
+ struct buffer value;
+ assert_true(ctrl_msg_find_tlv(&buf, OOB_TLV_PROBE_PARAMETER, &value));
+ assert_int_equal(BLEN(&value), extended_len);
+
+ struct oob_probe_parameter out = { 0 };
+ assert_true(oob_probe_parameter_read(&value, &out));
+ assert_true(out.timestamp == 0xdeadbeefULL);
+ assert_int_equal(out.flags, 0);
+ /* the unknown trailing field must have been consumed from the payload */
+ assert_int_equal(BLEN(&buf), 0);
+
+ gc_free(&gc);
+}
+
+/* A value shorter than the mandatory fields must be rejected, even when it
+ * holds enough bytes for some of the individual fields to read successfully. */
+static void
+test_probe_parameter_too_short(void **state)
+{
+ struct gc_arena gc = gc_new();
+ struct buffer buf = alloc_buf_gc(128, &gc);
+
+ /* 4 of the 12 mandatory value bytes: too short for the timestamp, but
+ * enough for a u32 read to succeed on its own */
+ assert_true(buf_write_u32(&buf, 0xdeadbeef));
+
+ struct oob_probe_parameter out = { 0 };
+ assert_false(oob_probe_parameter_read(&buf, &out));
+
+ gc_free(&gc);
+}
+
+/* A TLV header claiming more value bytes than the payload holds must be
+ * rejected by the scan rather than reported as found -- for the TLV being
+ * looked for as much as for one that would merely be skipped. */
+static void
+test_find_tlv_value_truncated(void **state)
+{
+ struct gc_arena gc = gc_new();
+ struct buffer value;
+
+ /* the wanted TLV declares 12 value bytes, only 4 are present */
+ struct buffer buf = alloc_buf_gc(128, &gc);
+ assert_true(ctrl_msg_tlv_write_header(&buf, OOB_TLV_PROBE_PARAMETER, false,
+ OOB_PROBE_PARAMETER_LEN));
+ assert_true(buf_write_u32(&buf, 0xdeadbeef));
+ assert_false(ctrl_msg_find_tlv(&buf, OOB_TLV_PROBE_PARAMETER, &value));
+
+ /* same defect on a TLV that would be skipped: the scan must not walk past
+ * the end of the payload looking for the next header */
+ struct buffer buf2 = alloc_buf_gc(128, &gc);
+ assert_true(ctrl_msg_tlv_write_header(&buf2, 0x7ff, false, 64));
+ assert_true(buf_write_u32(&buf2, 0));
+ assert_false(ctrl_msg_find_tlv(&buf2, OOB_TLV_PROBE_PARAMETER, &value));
+
+ gc_free(&gc);
+}
+
+/* Reading a TLV header must fail when the buffer holds less data than a
+ * complete 4-byte header (empty, or only the type field), rather than read
+ * past the available data. */
+static void
+test_tlv_header_truncated(void **state)
+{
+ struct gc_arena gc = gc_new();
+ struct buffer buf = alloc_buf_gc(128, &gc);
+
+ struct ctrl_msg_tlv_header hdr;
+
+ /* empty buffer */
+ assert_false(ctrl_msg_tlv_read_header(&buf, &hdr));
+
+ /* only the type field present, no length */
+ assert_true(buf_write_u16(&buf, OOB_TLV_PROBE_PARAMETER));
+ assert_false(ctrl_msg_tlv_read_header(&buf, &hdr));
+
+ gc_free(&gc);
+}
+
+int
+main(void)
+{
+ openvpn_unit_test_setup();
+ const struct CMUnitTest tests[] = {
+ cmocka_unit_test(test_probe_parameter_roundtrip),
+ cmocka_unit_test(test_probe_reply_roundtrip),
+ cmocka_unit_test(test_probe_reply_wire_format),
+ cmocka_unit_test(test_probe_parameter_forward_compat),
+ cmocka_unit_test(test_probe_parameter_too_short),
+ cmocka_unit_test(test_find_tlv_value_truncated),
+ cmocka_unit_test(test_tlv_header_truncated),
+ };
+
+ return cmocka_run_group_tests_name("oob tests", tests, NULL, NULL);
+}
--
To view, visit http://gerrit.openvpn.net/c/openvpn/+/1741?usp=email
To unsubscribe, or for help writing mail filters, visit http://gerrit.openvpn.net/settings?usp=email
Gerrit-MessageType: newpatchset
Gerrit-Project: openvpn
Gerrit-Branch: master
Gerrit-Change-Id: I1c8d302ac57c5603d622a7be14be369437388268
Gerrit-Change-Number: 1741
Gerrit-PatchSet: 9
Gerrit-Owner: stipa <[email protected]>
Gerrit-Reviewer: flichtenheld <[email protected]>
Gerrit-Reviewer: plaisthos <[email protected]>
Gerrit-CC: openvpn-devel <[email protected]>
Gerrit-Attention: flichtenheld <[email protected]>
Gerrit-Attention: stipa <[email protected]>
_______________________________________________
Openvpn-devel mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/openvpn-devel