Attention is currently required from: flichtenheld, plaisthos, ralf_lici, stipa.
Hello plaisthos, ralf_lici,
I'd like you to reexamine a change. Please visit
http://gerrit.openvpn.net/c/openvpn/+/1744?usp=email
to look at the new patch set (#20).
Change subject: oob: Answer probe requests on the server (P_CONTROL_OOB_V1)
......................................................................
oob: Answer probe requests on the server (P_CONTROL_OOB_V1)
Make a --mode server UDP listener answer an out-of-band probe request
without creating a session, so probing costs the server no state.
tls_pre_decrypt_lite() accepts P_CONTROL_OOB_V1 and returns the new
VERDICT_VALID_OOB_V1, and tls_wrap_oob_standalone() wraps a bare OOB
message with the usual tls-auth/tls-crypt wrapping but no reliability
fields, mirroring tls_reset_standalone(). do_pre_decrypt_check()
classifies the probe with oob_probe_request_check() and answers with a
probe reply that echoes the probe's request_id.
A reply is charged against the same rate limit as the reset replies,
once one is going to be sent, so a probe flood is capped like a reset
flood. A stale probe, one whose timestamp is outside --hand-window, is
still answered, but only within a separate budget of a twentieth of
--connect-freq-initial per period (at least one): a client with a
skewed clock can still probe, and a replayed probe gets no more than
that budget. The budget's limiter is quiet, as a replayed probe
hitting it is not worth a warning.
The reply carries, as its own session id, the stateless SYN cookie the
three-way handshake uses, so the server keeps nothing per probe and a
client can later start the handshake from the reply.
Change-Id: I930d3789e0313aa0c3bc51ee5fd1d108343d59f0
Signed-off-by: Lev Stipakov <[email protected]>
---
M src/openvpn/mudp.c
M src/openvpn/multi.c
M src/openvpn/multi.h
M src/openvpn/reflect_filter.c
M src/openvpn/reflect_filter.h
M src/openvpn/ssl.c
M src/openvpn/ssl_pkt.c
M src/openvpn/ssl_pkt.h
M tests/unit_tests/openvpn/test_pkt.c
9 files changed, 249 insertions(+), 8 deletions(-)
git pull ssh://gerrit.openvpn.net:29418/openvpn refs/changes/44/1744/20
diff --git a/src/openvpn/mudp.c b/src/openvpn/mudp.c
index de3d467..e97ac39 100644
--- a/src/openvpn/mudp.c
+++ b/src/openvpn/mudp.c
@@ -32,6 +32,7 @@
#include "memdbg.h"
#include "ssl_pkt.h"
+#include "oob.h"
#ifdef HAVE_SYS_INOTIFY_H
#include <sys/inotify.h>
@@ -87,6 +88,34 @@
"Reset packet from client, sending HMAC based reset
challenge", sock);
}
+/* Send an out-of-band probe reply back to the source of a probe request,
+ * synchronously and without keeping any state, mirroring the reset path. */
+static void
+send_probe_reply(struct multi_context *m, struct tls_pre_decrypt_state *state,
+ struct tls_auth_standalone *tas, const struct oob_probe_reply
*reply,
+ struct session_id *own_sid, struct link_socket *sock)
+{
+ struct gc_arena gc = gc_new();
+
+ /* The reply carries just the probe reply TLV */
+ struct buffer payload = alloc_buf_gc(128, &gc);
+ if (!oob_probe_reply_write(&payload, reply))
+ {
+ gc_free(&gc);
+ return;
+ }
+
+ /* OOB replies use the same control-channel wrapping as the request */
+ reset_packet_id_send(&state->tls_wrap_tmp.opt.packet_id.send);
+ state->tls_wrap_tmp.opt.packet_id.rec.initialized = true;
+
+ struct buffer buf = tls_wrap_oob_standalone(&state->tls_wrap_tmp, tas,
own_sid, &payload);
+ send_standalone_reply(m, &buf, "Server Probe", "Server probe from client,
sending probe reply",
+ sock);
+
+ gc_free(&gc);
+}
+
/* Returns true if this packet should create a new session */
static bool
@@ -203,6 +232,47 @@
return ret;
}
+ else if (verdict == VERDICT_VALID_OOB_V1)
+ {
+ /* Out-of-band server probe. state->newbuf points at the OOB message
+ * (read_control_auth has stripped the opcode, session id and any
+ * tls-auth/tls-crypt wrapping). Answer it without creating a session.
*/
+ struct oob_probe_request req;
+ enum oob_probe_verdict probe =
+ oob_probe_request_check(&state->newbuf, (uint64_t)now,
(uint64_t)handwindow, &req);
+ if (probe == OOB_PROBE_INVALID)
+ {
+ return false; /* malformed: silently drop */
+ }
+ /* A client whose clock is off by more than --hand-window still gets a
+ * few probes per period answered, which is also all a replayed probe
+ * can get. */
+ if (probe == OOB_PROBE_STALE &&
!reflect_filter_rate_limit_check(m->stale_probe_limiter))
+ {
+ return false;
+ }
+ /* The reply counts against the same limit as the reset replies,
charged
+ * only now that we know one is going to be sent. */
+ if (!reflect_filter_rate_limit_check(m->initial_rate_limiter))
+ {
+ return false;
+ }
+
+ /* the reply echoes the probe's request_id */
+ struct oob_probe_reply reply = { .request_id = req.request_id };
+
+ /* Our session id is a stateless SYN cookie (the same HMAC the
three-way
+ * handshake uses): we keep no per-probe state, and the reply can later
+ * also serve as the server's CONTROL_HARD_RESET_SERVER_V2, so a client
+ * can start the handshake from it. */
+ struct session_id sid =
+ calculate_session_id_hmac(state->peer_session_id, from, hmac_key,
handwindow, 0);
+
+ send_probe_reply(m, state, tas, &reply, &sid, sock);
+
+ /* An OOB probe never creates a session */
+ return false;
+ }
/* VERDICT_INVALID */
return false;
diff --git a/src/openvpn/multi.c b/src/openvpn/multi.c
index 3e72b92..a438a34 100644
--- a/src/openvpn/multi.c
+++ b/src/openvpn/multi.c
@@ -326,6 +326,12 @@
m->new_connection_limiter = frequency_limit_init(t->options.cf_max,
t->options.cf_per);
m->initial_rate_limiter =
initial_rate_limit_init(t->options.cf_initial_max,
t->options.cf_initial_per);
+ /* stale server probes: a twentieth of the initial-packet rate, at least 1
+ * per period (5 per 10 s by default, the wire protocol's own example).
+ * Quiet, as a replayed probe hitting the limit is not worth a warning. */
+ m->stale_probe_limiter =
+ initial_rate_limit_init(max_int(1, t->options.cf_initial_max / 20),
t->options.cf_initial_per);
+ m->stale_probe_limiter->quiet = true;
/*
* Allocate broadcast/multicast buffer list
@@ -689,6 +695,7 @@
ifconfig_pool_free(m->ifconfig_pool);
frequency_limit_free(m->new_connection_limiter);
initial_rate_limit_free(m->initial_rate_limiter);
+ initial_rate_limit_free(m->stale_probe_limiter);
multi_reap_free(m->reaper);
mroute_helper_free(m->route_helper);
multi_io_free(m->multi_io);
diff --git a/src/openvpn/multi.h b/src/openvpn/multi.h
index 6cb86c7..615290d 100644
--- a/src/openvpn/multi.h
+++ b/src/openvpn/multi.h
@@ -178,6 +178,7 @@
struct ifconfig_pool *ifconfig_pool;
struct frequency_limit *new_connection_limiter;
struct initial_packet_rate_limit *initial_rate_limiter;
+ struct initial_packet_rate_limit *stale_probe_limiter; /**< stale probe
request answers */
struct mroute_helper *route_helper;
struct multi_reap *reaper;
struct mroute_addr local;
diff --git a/src/openvpn/reflect_filter.c b/src/openvpn/reflect_filter.c
index e5226e4..4887f01 100644
--- a/src/openvpn/reflect_filter.c
+++ b/src/openvpn/reflect_filter.c
@@ -44,7 +44,7 @@
if (now > irl->last_period_reset + irl->period_length)
{
int64_t dropped = irl->curr_period_counter - irl->max_per_period;
- if (dropped > 0)
+ if (dropped > 0 && !irl->quiet)
{
msg(D_TLS_DEBUG_LOW,
"Dropped %" PRId64 " initial handshake packets"
@@ -60,7 +60,7 @@
bool over_limit = irl->curr_period_counter > irl->max_per_period;
- if (over_limit && !irl->warning_displayed)
+ if (over_limit && !irl->warning_displayed && !irl->quiet)
{
msg(M_WARN,
"Note: --connect-freq-initial %" PRId64 " %d rate limit "
@@ -89,7 +89,7 @@
struct initial_packet_rate_limit *irl;
- ALLOC_OBJ(irl, struct initial_packet_rate_limit);
+ ALLOC_OBJ_CLEAR(irl, struct initial_packet_rate_limit);
irl->max_per_period = max_per_period;
irl->period_length = period_length;
diff --git a/src/openvpn/reflect_filter.h b/src/openvpn/reflect_filter.h
index 84915e8c6..e996d07 100644
--- a/src/openvpn/reflect_filter.h
+++ b/src/openvpn/reflect_filter.h
@@ -44,6 +44,9 @@
/* we want to warn once per period that packets are being started to
* be dropped */
bool warning_displayed;
+
+ /* drop silently: no per-period warning or summary in the log */
+ bool quiet;
};
diff --git a/src/openvpn/ssl.c b/src/openvpn/ssl.c
index 8e2a459..e8a3d20 100644
--- a/src/openvpn/ssl.c
+++ b/src/openvpn/ssl.c
@@ -3702,7 +3702,8 @@
struct session_id sid; /* remote session ID */
/* verify legal opcode. An out-of-band packet has no control message id and
- * no ACK array, which is what we parse next. */
+ * no ACK array, which is what we parse next; do_pre_decrypt_check()
answers
+ * those. */
if (op < P_FIRST_OPCODE || op > P_LAST_OPCODE || opcode_is_oob(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 38fb8ed..d3463b8 100644
--- a/src/openvpn/ssl_pkt.c
+++ b/src/openvpn/ssl_pkt.c
@@ -169,7 +169,8 @@
{
ASSERT(ks->key_id >= 0 && ks->key_id <= P_KEY_ID_MASK);
ASSERT(opcode >= 0 && opcode <= P_LAST_OPCODE);
- /* OOB packets carry no message id or ACK array */
+ /* OOB packets carry no message id or ACK array; tls_wrap_oob_standalone()
+ * builds them */
ASSERT(!opcode_is_oob(opcode));
uint8_t header = (uint8_t)(ks->key_id | (opcode << P_OPCODE_SHIFT));
@@ -317,7 +318,8 @@
/* Allow only the reset packet or the first packet of the actual
handshake. */
if (op != P_CONTROL_HARD_RESET_CLIENT_V2 && op !=
P_CONTROL_HARD_RESET_CLIENT_V3
- && op != P_CONTROL_V1 && op != P_CONTROL_WKC_V1 && op != P_ACK_V1)
+ && op != P_CONTROL_V1 && op != P_CONTROL_WKC_V1 && op != P_ACK_V1
+ && !opcode_is_oob(op))
{
/*
* This can occur due to bogus data or DoS packets.
@@ -392,6 +394,10 @@
{
return VERDICT_VALID_WKC_V1;
}
+ else if (opcode_is_oob(op))
+ {
+ return VERDICT_VALID_OOB_V1;
+ }
else
{
return VERDICT_VALID_RESET_V2;
@@ -445,6 +451,29 @@
return buf;
}
+struct buffer
+tls_wrap_oob_standalone(struct tls_wrap_ctx *ctx, struct tls_auth_standalone
*tas,
+ struct session_id *own_sid, const struct buffer
*payload)
+{
+ /* Copy buffer here to point at the same data but allow tls_wrap_control
+ * to potentially change buf to point to another buffer without
+ * modifying the buffer in tas */
+ struct buffer buf = tas->workbuf;
+ ASSERT(buf_init(&buf, tas->frame.buf.headroom));
+
+ /* Out-of-band messages carry the payload directly, with no reliability
+ * or ACK fields. */
+ ASSERT(buf_copy(&buf, payload));
+
+ uint8_t header = (uint8_t)(P_CONTROL_OOB_V1 << P_OPCODE_SHIFT);
+
+ /* Add tls-auth/tls-crypt wrapping, this might replace buf with
+ * ctx->work */
+ tls_wrap_control(ctx, header, &buf, own_sid);
+
+ return buf;
+}
+
struct session_id
calculate_session_id_hmac(struct session_id client_sid, const struct
openvpn_sockaddr *from,
const uint8_t *key, int handwindow, int offset)
diff --git a/src/openvpn/ssl_pkt.h b/src/openvpn/ssl_pkt.h
index d477d5a..5060a4a 100644
--- a/src/openvpn/ssl_pkt.h
+++ b/src/openvpn/ssl_pkt.h
@@ -59,8 +59,9 @@
#define P_CONTROL_WKC_V1 11
/* Out-of-band control message, e.g. a server probe. Not part of the reliable
- * control channel: no control message id and no ACK array, just TLVs
- * after the session id; never handled by tls_pre_decrypt(). */
+ * control channel: no control message id and no ACK array, just TLVs after
+ * the session id. Answered statelessly in do_pre_decrypt_check(), never
+ * handled by tls_pre_decrypt(). */
#define P_CONTROL_OOB_V1 12
/* define the range of defined opcodes, in- and out-of-band
@@ -106,6 +107,9 @@
VERDICT_VALID_ACK_V1,
/** The packet is a valid control packet with appended wrapped client key
*/
VERDICT_VALID_WKC_V1,
+ /** This packet is a valid out-of-band control message (e.g. a server
+ * probe). It does not belong to a session and must not create one. */
+ VERDICT_VALID_OOB_V1,
/** the packet failed on of the various checks */
VERDICT_INVALID
};
@@ -226,6 +230,21 @@
struct session_id *own_sid, struct
session_id *remote_sid,
uint8_t header, bool request_resend_wkc);
+/**
+ * Wrap an already-built out-of-band message (e.g. a probe reply) into a
+ * standalone, session-less P_CONTROL_OOB_V1 packet: it prepends the opcode and
+ * own_sid and applies the same tls-auth/tls-crypt wrapping as a regular
+ * control packet, but carries no reliability/ACK fields.
+ *
+ * @param ctx tls wrapping context (from the pre-decrypt state)
+ * @param tas standalone auth context providing the work buffer
+ * @param own_sid session id to use as our session id in the header
+ * @param payload the OOB message (header and TLVs) to wrap
+ * @return the wrapped packet buffer, ready to send
+ */
+struct buffer tls_wrap_oob_standalone(struct tls_wrap_ctx *ctx, struct
tls_auth_standalone *tas,
+ struct session_id *own_sid, const struct
buffer *payload);
+
/**
* Extracts a control channel message from buf and adjusts the size of
diff --git a/tests/unit_tests/openvpn/test_pkt.c
b/tests/unit_tests/openvpn/test_pkt.c
index 0398a1f..887526b 100644
--- a/tests/unit_tests/openvpn/test_pkt.c
+++ b/tests/unit_tests/openvpn/test_pkt.c
@@ -739,6 +739,114 @@
free_tas(&tas_server);
}
+/* Payload every OOB round-trip below wraps: one TLV (type 1, 4 bytes of
+ * value). */
+static const uint8_t oob_payload[] = { 0x00, 0x01, 0x00, 0x04, 0xde, 0xad,
0xbe, 0xef };
+
+/* Wrap oob_payload as a standalone P_CONTROL_OOB_V1 with the client side of a
+ * tls-auth/tls-crypt pair (or none), run it through the server's stateless
+ * first-packet path, and check verdict, recovered session id and payload. */
+static void
+oob_standalone_roundtrip(struct tls_auth_standalone *tas_client,
+ struct tls_auth_standalone *tas_server)
+{
+ struct link_socket_actual from = { 0 };
+ struct tls_pre_decrypt_state state = { 0 };
+ struct session_id sid = { { 0x0b, 1, 2, 3, 4, 5, 6, 0x0b } };
+
+ struct buffer payload = alloc_buf(64);
+ buf_write(&payload, oob_payload, sizeof(oob_payload));
+
+ /* Client side: opcode + session id + tls-auth/tls-crypt wrapping around
the
+ * bare payload, with none of the reliability/ACK fields a control packet
+ * carries. */
+ struct buffer buf =
+ tls_wrap_oob_standalone(&tas_client->tls_wrap, tas_client, &sid,
&payload);
+ assert_int_equal((BPTR(&buf))[0] >> P_OPCODE_SHIFT, P_CONTROL_OOB_V1);
+
+ /* Server side: the stateless first-packet path must recognise the opcode,
+ * verify the wrapping and leave exactly the payload in newbuf -- that is
+ * what mudp.c hands to the probe parser. */
+ enum first_packet_verdict verdict = tls_pre_decrypt_lite(tas_server,
&state, &from, &buf);
+ assert_int_equal(verdict, VERDICT_VALID_OOB_V1);
+ assert_memory_equal(state.peer_session_id.id, sid.id, SID_SIZE);
+ assert_int_equal(BLEN(&state.newbuf), (int)sizeof(oob_payload));
+ assert_memory_equal(BPTR(&state.newbuf), oob_payload, sizeof(oob_payload));
+ free_tls_pre_decrypt_state(&state);
+
+ /* Tampering with any single byte must fail authentication: the flip runs
+ * over opcode, session id, packet id, HMAC/tag and payload alike. Skipped
+ * for TLS_WRAP_NONE, where a changed payload is legitimately accepted. */
+ if (tas_server->tls_wrap.mode != TLS_WRAP_NONE)
+ {
+ struct buffer copy = alloc_buf(BLEN(&buf));
+ for (int i = 0; i < BLEN(&buf); i++)
+ {
+ buf_reset_len(©);
+ buf_write(©, BPTR(&buf), BLEN(&buf));
+ (BPTR(©))[i] ^= 0xff;
+ struct tls_pre_decrypt_state tstate = { 0 };
+ verdict = tls_pre_decrypt_lite(tas_server, &tstate, &from, ©);
+ assert_int_equal(verdict, VERDICT_INVALID);
+ free_tls_pre_decrypt_state(&tstate);
+ }
+ free_buf(©);
+ }
+
+ free_buf(&payload);
+}
+
+static void
+test_oob_standalone_plain(void **ut_state)
+{
+ struct tls_auth_standalone tas = { 0 };
+ struct frame frame = { .buf = { .headroom = 200, .payload_size = 1400 }, 0
};
+ tas.frame = frame;
+ tas.tls_wrap.mode = TLS_WRAP_NONE;
+ tas.workbuf = alloc_buf(1600);
+
+ oob_standalone_roundtrip(&tas, &tas);
+
+ free_tas(&tas);
+}
+
+static void
+test_oob_standalone_tls_auth(void **ut_state)
+{
+ struct tls_auth_standalone tas_server =
init_tas_auth(KEY_DIRECTION_NORMAL);
+ struct tls_auth_standalone tas_client =
init_tas_auth(KEY_DIRECTION_INVERSE);
+ packet_id_init(&tas_client.tls_wrap.opt.packet_id, 5, 5, "UNITTEST", 0);
+ /* the server consumes the tls-auth packet id only with packet-id state,
+ * which tls_auth_standalone_init() sets up for the real one */
+ packet_id_init(&tas_server.tls_wrap.opt.packet_id, 5, 5, "UNITTEST", 0);
+
+ now = 0x22446688;
+ oob_standalone_roundtrip(&tas_client, &tas_server);
+
+ packet_id_free(&tas_client.tls_wrap.opt.packet_id);
+ packet_id_free(&tas_server.tls_wrap.opt.packet_id);
+ free_tas(&tas_client);
+ free_tas(&tas_server);
+}
+
+static void
+test_oob_standalone_tls_crypt(void **ut_state)
+{
+ struct frame frame = { .buf = { .headroom = 200, .payload_size = 1400 }, 0
};
+ struct tls_auth_standalone tas_server = init_tas_crypt(true);
+ struct tls_auth_standalone tas_client = init_tas_crypt(false);
+ tas_server.frame = frame;
+ tas_client.frame = frame;
+ packet_id_init(&tas_client.tls_wrap.opt.packet_id, 5, 5, "UNITTEST", 0);
+
+ now = 0x22446688;
+ oob_standalone_roundtrip(&tas_client, &tas_server);
+
+ packet_id_free(&tas_client.tls_wrap.opt.packet_id);
+ free_tas(&tas_client);
+ free_tas(&tas_server);
+}
+
static void
test_extract_control_message(void **ut_state)
{
@@ -790,6 +898,9 @@
cmocka_unit_test(test_generate_reset_packet_plain),
cmocka_unit_test(test_generate_reset_packet_resend_wkc),
cmocka_unit_test(test_generate_reset_packet_tls_auth),
+ cmocka_unit_test(test_oob_standalone_plain),
+ cmocka_unit_test(test_oob_standalone_tls_auth),
+ cmocka_unit_test(test_oob_standalone_tls_crypt),
cmocka_unit_test(test_extract_control_message)
};
--
To view, visit http://gerrit.openvpn.net/c/openvpn/+/1744?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: I930d3789e0313aa0c3bc51ee5fd1d108343d59f0
Gerrit-Change-Number: 1744
Gerrit-PatchSet: 20
Gerrit-Owner: stipa <[email protected]>
Gerrit-Reviewer: plaisthos <[email protected]>
Gerrit-Reviewer: ralf_lici <[email protected]>
Gerrit-CC: flichtenheld <[email protected]>
Gerrit-CC: openvpn-devel <[email protected]>
Gerrit-Attention: ralf_lici <[email protected]>
Gerrit-Attention: stipa <[email protected]>
Gerrit-Attention: plaisthos <[email protected]>
Gerrit-Attention: flichtenheld <[email protected]>
_______________________________________________
Openvpn-devel mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/openvpn-devel