From 662bf7833e03a89b56ddc984d21e12e0fcb6b5ab Mon Sep 17 00:00:00 2001 From: James Yonan Date: Thu, 1 Sep 2016 15:19:00 -0600 Subject: [PATCH] ovpn3 core : Added automatic data limits for Blowfish, Triple DES, and other 64-bit block-size ciphers vulnerable to "Sweet32" birthday attack (CVE-2016-6329). Limit such cipher keys to no more than 64 MB of data encrypted/decrypted. While our overall goal is to limit data-limited keys to 64 MB, we trigger a renegotiation at 48 MB to compensate for possible delays in renegotiation and rollover to the new key. This client-side implementation extends data limit protection to the entire session, even when the server doesn't implement data limits. This capability is advertised to servers via the a peer info setting: IV_BS64DL=1 meaning "Block-Size 64-bit Data Limit". The "1" indicates the implementation version. The implementation currently has some limitations: * Keys are renegotiated at a maximum rate of once per 5 seconds to reduce the likelihood of loss of synchronization between peers. * The maximum renegotiation rate may be further extended if the peer delays rollover from the old to new key after renegotiation. Added N_KEY_LIMIT_RENEG stats counter to count the number of data-limit-triggered renegotiations. Added new stats counter KEY_STATE_ERROR which roughly corresponds to the OpenVPN 2.x error "TLS Error: local/remote TLS keys are out of sync". Prevously, the TLS ack/retransmit timeout was hardcoded to 2 seconds. Now we lower the default to 1 second and make it variable using the (pushable) "tls-timeout" directive. Additionally, the tls-timeout directive can be specified in milliseconds instead of seconds by using the "tls-timeout-ms" form of the directive. Made the "become primary" time duration configurable via the (pushable) "become-primary" directive which accepts a number-of-seconds parameter. become-primary indicates the time delay between renegotiation and rollover to the new key for encryption/transmission. become-primary defaults to the handshake-window which in turn defaults to 60 seconds. Incremented core version to 3.0.20. --- openvpn/client/cliproto.hpp | 4 +- openvpn/common/version.hpp | 2 +- openvpn/crypto/bs64_data_limit.hpp | 45 +++ openvpn/error/error.hpp | 10 +- openvpn/reliable/relsend.hpp | 12 +- openvpn/server/servproto.hpp | 10 + openvpn/ssl/datalimit.hpp | 188 ++++++++++ openvpn/ssl/proto.hpp | 474 +++++++++++++++++-------- openvpn/ssl/protostack.hpp | 11 +- openvpn/time/durhelper.hpp | 45 ++- openvpn/transport/server/transbase.hpp | 7 + test/ovpncli/go | 1 + test/ssl/proto.cpp | 169 +++++++-- 13 files changed, 776 insertions(+), 202 deletions(-) create mode 100644 openvpn/crypto/bs64_data_limit.hpp create mode 100644 openvpn/ssl/datalimit.hpp diff --git a/openvpn/client/cliproto.hpp b/openvpn/client/cliproto.hpp index 5bdf4bae..482af4a7 100644 --- a/openvpn/client/cliproto.hpp +++ b/openvpn/client/cliproto.hpp @@ -296,6 +296,8 @@ namespace openvpn { // do a full flush Base::flush(true); } + else + cli_stats->error(Error::KEY_STATE_ERROR); // schedule housekeeping wakeup set_housekeeping_timer(); @@ -783,7 +785,7 @@ namespace openvpn { void process_inactive(const OptionList& opt) { try { - const Option *o = load_duration_parm(inactive_duration, "inactive", opt, 1, false); + const Option *o = load_duration_parm(inactive_duration, "inactive", opt, 1, false, false); if (o) { if (o->size() >= 3) diff --git a/openvpn/common/version.hpp b/openvpn/common/version.hpp index 2a7c435d..bbb4cc32 100644 --- a/openvpn/common/version.hpp +++ b/openvpn/common/version.hpp @@ -24,6 +24,6 @@ #ifndef OPENVPN_COMMON_VERSION_H #define OPENVPN_COMMON_VERSION_H -#define OPENVPN_VERSION "3.0.19" +#define OPENVPN_VERSION "3.0.20" #endif // OPENVPN_COMMON_VERSION_H diff --git a/openvpn/crypto/bs64_data_limit.hpp b/openvpn/crypto/bs64_data_limit.hpp new file mode 100644 index 00000000..af2ca4ef --- /dev/null +++ b/openvpn/crypto/bs64_data_limit.hpp @@ -0,0 +1,45 @@ +// OpenVPN -- An application to securely tunnel IP networks +// over a single port, with support for SSL/TLS-based +// session authentication and key exchange, +// packet encryption, packet authentication, and +// packet compression. +// +// Copyright (C) 2012-2015 OpenVPN Technologies, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License Version 3 +// 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program in the COPYING file. +// If not, see . + +// Special data limits on Blowfish, Triple DES, and other 64-bit +// block-size ciphers vulnerable to "Sweet32" birthday attack +// (CVE-2016-6329). Limit such cipher keys to no more than 64 MB +// of data encrypted/decrypted. Note that we trigger early at +// 48 MB to compensate for possible delays in renegotiation and +// rollover to the new key. + +#ifndef OPENVPN_CRYPTO_DATALIMIT_H +#define OPENVPN_CRYPTO_DATALIMIT_H + +#include + +#ifndef OPENVPN_BS64_DATA_LIMIT +#define OPENVPN_BS64_DATA_LIMIT 48000000 +#endif + +namespace openvpn { + inline bool is_bs64_cipher(const CryptoAlgs::Type cipher) + { + return CryptoAlgs::get(cipher).block_size() == 8; + } +} + +#endif diff --git a/openvpn/error/error.hpp b/openvpn/error/error.hpp index f2eb9ba2..e89f0715 100644 --- a/openvpn/error/error.hpp +++ b/openvpn/error/error.hpp @@ -77,12 +77,15 @@ namespace openvpn { CLIENT_RESTART, // RESTART message from server received N_PAUSE, // Number of transitions to Pause state N_RECONNECT, // Number of reconnections + N_KEY_LIMIT_RENEG, // Number of renegotiations triggered by per-key limits such as data or packet limits + KEY_STATE_ERROR, // Received packet didn't match expected key state PROXY_ERROR, // HTTP proxy error PROXY_NEED_CREDS, // HTTP proxy needs credentials // key event errors KEV_NEGOTIATE_ERROR, - KEV_EXPIRE_ERROR, + KEV_PENDING_ERROR, + N_KEV_EXPIRE, // Packet ID error detail PKTID_INVALID, @@ -147,10 +150,13 @@ namespace openvpn { "CLIENT_RESTART", "N_PAUSE", "N_RECONNECT", + "N_KEY_LIMIT_RENEG", + "KEY_STATE_ERROR", "PROXY_ERROR", "PROXY_NEED_CREDS", "KEV_NEGOTIATE_ERROR", - "KEV_EXPIRE_ERROR", + "KEV_PENDING_ERROR", + "N_KEV_EXPIRE", "PKTID_INVALID", "PKTID_BACKTRACK", "PKTID_EXPIRE", diff --git a/openvpn/reliable/relsend.hpp b/openvpn/reliable/relsend.hpp index 7561cc15..3e5bee43 100644 --- a/openvpn/reliable/relsend.hpp +++ b/openvpn/reliable/relsend.hpp @@ -38,10 +38,6 @@ namespace openvpn { public: typedef reliable::id_t id_t; - enum { - RETRANSMIT = 2 // retransmit in N seconds if ACK not received - }; - class Message : public ReliableMessageBase { friend class ReliableSendTemplate; @@ -61,9 +57,9 @@ namespace openvpn { return ret; } - void reset_retransmit(const Time& now) + void reset_retransmit(const Time& now, const Time::Duration& tls_timeout) { - retransmit_at_ = now + Time::Duration::seconds(RETRANSMIT); + retransmit_at_ = now + tls_timeout; } private: @@ -128,11 +124,11 @@ namespace openvpn { // Return a fresh Message object that can be used to // construct the next packet in the sequence. Don't call // unless ready() returns true. - Message& send(const Time& now) + Message& send(const Time& now, const Time::Duration& tls_timeout) { Message& msg = window_.ref_by_id(next); msg.id_ = next++; - msg.reset_retransmit(now); + msg.reset_retransmit(now, tls_timeout); return msg; } diff --git a/openvpn/server/servproto.hpp b/openvpn/server/servproto.hpp index ca87d3c9..84759b84 100644 --- a/openvpn/server/servproto.hpp +++ b/openvpn/server/servproto.hpp @@ -499,6 +499,16 @@ namespace openvpn { ManLink::send->float_notify(addr); } + virtual void data_limit_notify(const int key_id, + const DataLimit::Mode cdl_mode, + const DataLimit::State cdl_status) + { + Base::update_now(); + Base::data_limit_notify(key_id, cdl_mode, cdl_status); + Base::flush(true); + set_housekeeping_timer(); + } + bool get_management() { if (!ManLink::send) diff --git a/openvpn/ssl/datalimit.hpp b/openvpn/ssl/datalimit.hpp new file mode 100644 index 00000000..151db073 --- /dev/null +++ b/openvpn/ssl/datalimit.hpp @@ -0,0 +1,188 @@ +// OpenVPN +// Copyright (C) 2012-2015 OpenVPN Technologies, Inc. +// All rights reserved + +#ifndef OPENVPN_SSL_DATALIMIT_H +#define OPENVPN_SSL_DATALIMIT_H + +#include + +namespace openvpn { + // Helper for handling keys which can have an upper limit + // on maximum amount of data encrypted/decrypted, such + // as Blowfish. + class DataLimit + { + public: + typedef unsigned int size_type; + + enum Mode { + Encrypt=0, + Decrypt=1, + }; + + enum State { + None=0, + Green=1, + Red=2, + }; + + struct Parameters + { + size_type encrypt_red_limit = 0; + size_type decrypt_red_limit = 0; + }; + + DataLimit(const Parameters& p) + : encrypt(p.encrypt_red_limit), + decrypt(p.decrypt_red_limit) + { + } + + State update_state(const Mode mode, const State newstate) + { + return elgible(mode, component(mode).update_state(newstate)); + } + + State add(const Mode mode, const size_type n) + { + return elgible(mode, component(mode).add(n)); + } + + bool is_decrypt_green() + { + return decrypt.get_state() >= Green; + } + + static const char *mode_str(const Mode m) + { + switch (m) + { + case Encrypt: + return "Encrypt"; + case Decrypt: + return "Decrypt"; + default: + return "Mode_???"; + } + } + + static const char *state_str(const State s) + { + switch (s) + { + case None: + return "None"; + case Green: + return "Green"; + case Red: + return "Red"; + default: + return "State_???"; + } + } + + private: + // Don't return Encrypt-Red until Decrypt-Green + // has been received. This confirms that the peer + // is now transmitting on the key ID, making it + // eligible for renegotiation. + State elgible(const Mode mode, const State state) + { + // Bit positions for Encrypt/Decrypt and Green/Red + enum { + EG = 1<<0, + ER = 1<<1, + DG = 1<<2, + DR = 1<<3, + }; + if (state > None) + { + const unsigned int mask = 1 << ((int(state) - 1) + (int(mode) << 1)); + if (!(flags & mask)) + { + flags |= mask; + if ((mask & (ER|DG)) && ((flags & (ER|DG)) == (ER|DG))) + return Red; + else if (mask & ER) + return None; + else + return state; + } + } + return None; + } + + class Component + { + public: + Component(const size_type red_limit_arg) + : red_limit(red_limit_arg) + { + } + + State add(const size_type n) + { + bytes += n; + return update_state(transition(state)); + } + + State update_state(const State newstate) + { + State ret = None; + if (newstate > state) + state = ret = newstate; + return ret; + } + + State get_state() const + { + return state; + } + + private: + State transition(State s) const + { + switch (s) + { + case None: + if (bytes) + return Green; + else + return None; + case Green: + if (red_limit && bytes >= red_limit) + return Red; + else + return None; + case Red: + default: + return None; + } + } + + const size_type red_limit; + size_type bytes = 0; + State state = None; + }; + + Component& component(const Mode m) + { + switch (m) + { + case Encrypt: + return encrypt; + case Decrypt: + return decrypt; + default: + throw Exception("DataLimit::Component: unknown mode"); + } + } + + Component encrypt; + Component decrypt; + unsigned int flags = 0; + }; +} + +#endif diff --git a/openvpn/ssl/proto.hpp b/openvpn/ssl/proto.hpp index 36284c77..bebefeed 100644 --- a/openvpn/ssl/proto.hpp +++ b/openvpn/ssl/proto.hpp @@ -44,6 +44,7 @@ #include #include #include +#include #include #include #include @@ -57,10 +58,12 @@ #include #include #include +#include #include #include #include #include +#include #include #include #include @@ -300,6 +303,7 @@ namespace openvpn { Time::Duration become_primary; // KeyContext (that is ACTIVE) becomes primary at this time Time::Duration renegotiate; // start SSL/TLS renegotiation at this time Time::Duration expire; // KeyContext expires at this time + Time::Duration tls_timeout; // Packet retransmit timeout on TLS control channel // keepalive parameters Time::Duration keepalive_ping; @@ -333,6 +337,7 @@ namespace openvpn { max_ack_list = 4; handshake_window = Time::Duration::seconds(60); renegotiate = Time::Duration::seconds(3600); + tls_timeout = Time::Duration::seconds(1); keepalive_ping = Time::Duration::seconds(8); keepalive_timeout = Time::Duration::seconds(40); comp_ctx = CompressContext(CompressContext::NONE, false); @@ -340,9 +345,6 @@ namespace openvpn { pid_mode = PacketIDReceive::UDP_MODE; key_direction = default_key_direction; - // load parameters that can be present in both config file or pushed options - load_common(opt, pco, server ? LOAD_COMMON_SERVER : LOAD_COMMON_CLIENT); - // layer { const Option* dev = opt.get_ptr("dev-type"); @@ -467,20 +469,14 @@ namespace openvpn { // tun-mtu tun_mtu = parse_tun_mtu(opt, tun_mtu); + + // load parameters that can be present in both config file or pushed options + load_common(opt, pco, server ? LOAD_COMMON_SERVER : LOAD_COMMON_CLIENT); } // load options string pushed by server void process_push(const OptionList& opt, const ProtoContextOptions& pco) { - try { - // load parameters that can be present in both config file or pushed options - load_common(opt, pco, LOAD_COMMON_CLIENT_PUSHED); - } - catch (const std::exception& e) - { - OPENVPN_THROW(process_server_push_error, "Problem accepting server-pushed parameter: " << e.what()); - } - // data channel { // cipher @@ -569,6 +565,15 @@ namespace openvpn { OPENVPN_THROW(process_server_push_error, "Problem accepting server-pushed peer-id: " << e.what()); } + try { + // load parameters that can be present in both config file or pushed options + load_common(opt, pco, LOAD_COMMON_CLIENT_PUSHED); + } + catch (const std::exception& e) + { + OPENVPN_THROW(process_server_push_error, "Problem accepting server-pushed parameter: " << e.what()); + } + // show negotiated options OPENVPN_LOG_STRING_PROTO(show_options()); } @@ -687,6 +692,8 @@ namespace openvpn { out << compstr; if (extra_peer_info) out << extra_peer_info->to_string(); + if (is_bs64_cipher(dc.cipher())) + out << "IV_BS64DL=1\n"; // indicate support for data limits when using 64-bit block-size ciphers, version 1 (CVE-2016-6329) const std::string ret = out.str(); OPENVPN_LOG_PROTO("Peer Info:" << std::endl << ret); return ret; @@ -716,13 +723,22 @@ namespace openvpn { const LoadCommonType type) { // duration parms - load_duration_parm(renegotiate, "reneg-sec", opt, 10, false); + load_duration_parm(renegotiate, "reneg-sec", opt, 10, false, false); expire = renegotiate; - load_duration_parm(expire, "tran-window", opt, 10, false); + load_duration_parm(expire, "tran-window", opt, 10, false, false); expire += renegotiate; - load_duration_parm(handshake_window, "hand-window", opt, 10, false); - become_primary = Time::Duration::seconds(std::min(handshake_window.to_seconds(), + load_duration_parm(handshake_window, "hand-window", opt, 10, false, false); + if (is_bs64_cipher(dc.cipher())) // special data limits for 64-bit block-size ciphers (CVE-2016-6329) + { + become_primary = Time::Duration::seconds(5); + tls_timeout = Time::Duration::milliseconds(1000); + } + else + become_primary = Time::Duration::seconds(std::min(handshake_window.to_seconds(), renegotiate.to_seconds() / 2)); + load_duration_parm(become_primary, "become-primary", opt, 0, false, false); + load_duration_parm(tls_timeout, "tls-timeout", opt, 100, false, true); + if (type == LOAD_COMMON_SERVER) renegotiate += handshake_window; // avoid renegotiation collision with client @@ -731,13 +747,13 @@ namespace openvpn { const Option *o = opt.get_ptr("keepalive"); if (o) { - set_duration_parm(keepalive_ping, "keepalive ping", o->get(1, 16), 1, false); - set_duration_parm(keepalive_timeout, "keepalive timeout", o->get(2, 16), 1, type == LOAD_COMMON_SERVER); + set_duration_parm(keepalive_ping, "keepalive ping", o->get(1, 16), 1, false, false); + set_duration_parm(keepalive_timeout, "keepalive timeout", o->get(2, 16), 1, type == LOAD_COMMON_SERVER, false); } else { - load_duration_parm(keepalive_ping, "ping", opt, 1, false); - load_duration_parm(keepalive_timeout, "ping-restart", opt, 1, false); + load_duration_parm(keepalive_ping, "ping", opt, 1, false, false); + load_duration_parm(keepalive_timeout, "ping-restart", opt, 1, false, false); } } } @@ -834,8 +850,6 @@ namespace openvpn { } } - bool is_secondary() const { return flags & SECONDARY; } - unsigned int flags; unsigned int opcode; int peer_id_; @@ -1112,25 +1126,101 @@ namespace openvpn { public: typedef RCPtr Ptr; - // timeline of events for KeyContext (occurring in order) + // KeyContext events occur on two basic key types: + // Primary Key -- the key we transmit/encrypt on. + // Secondary Key -- new keys and retiring keys. + // + // The very first key created (key_id == 0) is a + // primary key. Subsequently created keys are always, + // at least initially, secondary keys. Secondary keys + // promote to primary via the KEV_BECOME_PRIMARY event + // (actually KEV_BECOME_PRIMARY swaps the primary and + // secondary keys, so the old primary is demoted + // to secondary and marked for expiration). + // + // Secondary keys are created by: + // 1. locally-generated soft renegotiation requests, and + // 2. peer-requested soft renegotiation requests. + // In each case, any previous secondary key will be + // wiped (including a secondary key that exists due to + // demotion of a previous primary key that has been marked + // for expiration). enum EventType { KEV_NONE, - KEV_ACTIVE, // KeyContext has reached the ACTIVE state - KEV_NEGOTIATE, // SSL/TLS negotiation must complete by this time - KEV_BECOME_PRIMARY, // KeyContext becomes primary for data channel traffic - KEV_RENEGOTIATE, // start renegotiating a new KeyContext at this time - KEV_EXPIRE, // expiration of KeyContext - KEV_NEGOTIATE_FAILED, // SSL/TLS negotiation failed + + // KeyContext has reached the ACTIVE state, occurs on both + // primary and secondary. + KEV_ACTIVE, + + // SSL/TLS negotiation must complete by this time. If this + // event is hit on the first primary (i.e. first KeyContext + // with key_id == 0), it is fatal to the session and will + // trigger a disconnect/reconnect. If it's hit on the + // secondary, it will trigger a soft renegotiation. + KEV_NEGOTIATE, + + // When a KeyContext (normally the secondary) is scheduled + // to transition to the primary state. + KEV_BECOME_PRIMARY, + + // Waiting for condition on secondary (usually + // dataflow-based) to trigger KEV_BECOME_PRIMARY. + KEV_PRIMARY_PENDING, + + // Start renegotiating a new KeyContext on secondary + // (ignored unless originating on primary). + KEV_RENEGOTIATE, + + // Trigger a renegotiation originating from either + // primary or secondary. + KEV_RENEGOTIATE_FORCE, + + // Queue delayed renegotiation request from secondary + // to take effect after KEV_BECOME_PRIMARY. + KEV_RENEGOTIATE_QUEUE, + + // Expiration of KeyContext. + KEV_EXPIRE, }; + // for debugging + static const char *event_type_string(const EventType et) + { + switch (et) + { + case KEV_NONE: + return "KEV_NONE"; + case KEV_ACTIVE: + return "KEV_ACTIVE"; + case KEV_NEGOTIATE: + return "KEV_NEGOTIATE"; + case KEV_BECOME_PRIMARY: + return "KEV_BECOME_PRIMARY"; + case KEV_PRIMARY_PENDING: + return "KEV_PRIMARY_PENDING"; + case KEV_RENEGOTIATE: + return "KEV_RENEGOTIATE"; + case KEV_RENEGOTIATE_FORCE: + return "KEV_RENEGOTIATE_FORCE"; + case KEV_RENEGOTIATE_QUEUE: + return "KEV_RENEGOTIATE_QUEUE"; + case KEV_EXPIRE: + return "KEV_EXPIRE"; + default: + return "KEV_?"; + } + } + KeyContext(ProtoContext& p, const bool initiator) - : Base(*p.config->ssl_factory, p.config->now, p.config->frame, p.stats, + : Base(*p.config->ssl_factory, + p.config->now, p.config->tls_timeout, + p.config->frame, p.stats, p.config->reliable_window, p.config->max_ack_list), proto(p), state(STATE_UNDEF), crypto_flags(0), dirty(0), - handled_pid_wrap(false), + key_limit_renegotiation_fired(false), is_reliable(p.config->protocol.is_reliable()), tlsprf(p.config->tlsprf_factory->new_obj(p.is_server())) { @@ -1232,8 +1322,12 @@ namespace openvpn { // compress and encrypt packet and prepend op header const bool pid_wrap = do_encrypt(buf, true); - // check for rare situation where packet ID is near overflow - test_pid_wrap(pid_wrap); + // Trigger a new SSL/TLS negotiation if packet ID (a 32-bit unsigned int) + // is getting close to wrapping around. If it wraps back to 0 without + // a renegotiation, it would cause the relay protection logic to wrongly + // think that all further packets are replays. + if (pid_wrap) + schedule_key_limit_renegotiation(); } else buf.reset_size(); // no crypto context available @@ -1262,6 +1356,10 @@ namespace openvpn { invalidate(err); } + // trigger renegotiation if we hit decrypt data limit + if (data_limit) + data_limit_add(DataLimit::Decrypt, buf.size()); + // decompress packet if (compress) compress->decompress(buf); @@ -1280,9 +1378,34 @@ namespace openvpn { // usually called by parent ProtoContext object when this KeyContext // has been retired. - void prepare_expire() + void prepare_expire(const EventType current_ev = KeyContext::KEV_NONE) { - set_event(KEV_NONE, KEV_EXPIRE, construct_time + proto.config->expire); + set_event(current_ev, + KEV_EXPIRE, + key_limit_renegotiation_fired ? data_limit_expire() : construct_time + proto.config->expire); + } + + // set a default next event, if unspecified + void set_next_event_if_unspecified() + { + if (next_event == KEV_NONE && !invalidated()) + prepare_expire(); + } + + // set a key limit renegotiation event at time t + void key_limit_reneg(const EventType ev, const Time& t) + { + if (t.defined()) + set_event(KEV_NONE, ev, t + Time::Duration::seconds(proto.is_server() ? 2 : 1)); + } + + // return time of upcoming KEV_BECOME_PRIMARY event + Time become_primary_time() + { + if (next_event == KEV_BECOME_PRIMARY) + return next_event_time; + else + return Time(); } // is an KEV_x event pending? @@ -1297,7 +1420,7 @@ namespace openvpn { EventType get_event() const { return current_event; } // clear KEV_x event - void reset_event() { set_event(KEV_NONE); } + void reset_event() { current_event = KEV_NONE; } // was session invalidated by an exception? bool invalidated() const { return Base::invalidated(); } @@ -1455,6 +1578,16 @@ namespace openvpn { const unsigned int key_dir = proto.is_server() ? OpenVPNStaticKey::INVERSE : OpenVPNStaticKey::NORMAL; const OpenVPNStaticKey& key = data_channel_key->key; + // special data limits for 64-bit block-size ciphers (CVE-2016-6329) + if (is_bs64_cipher(c.dc.cipher())) + { + DataLimit::Parameters dp; + dp.encrypt_red_limit = OPENVPN_BS64_DATA_LIMIT; + dp.decrypt_red_limit = OPENVPN_BS64_DATA_LIMIT; + OPENVPN_LOG_PROTO("Per-Key Data Limit: " << dp.encrypt_red_limit << '/' << dp.decrypt_red_limit); + data_limit.reset(new DataLimit(dp)); + } + // build crypto context for data channel encryption/decryption crypto = c.dc.context().new_obj(key_id_); crypto_flags = crypto->defined(); @@ -1490,6 +1623,13 @@ namespace openvpn { } } + void data_limit_notify(const DataLimit::Mode cdl_mode, + const DataLimit::State cdl_status) + { + if (data_limit) + data_limit_event(cdl_mode, data_limit->update_state(cdl_mode, cdl_status)); + } + private: bool do_encrypt(BufferAllocated& buf, const bool compress_hint) { @@ -1499,6 +1639,10 @@ namespace openvpn { if (compress) compress->compress(buf, compress_hint); + // trigger renegotiation if we hit encrypt data limit + if (data_limit) + data_limit_add(DataLimit::Encrypt, buf.size()); + if (enable_op32) { const std::uint32_t op32 = htonl(op32_compose(DATA_V2, key_id_, remote_peer_id)); @@ -1531,26 +1675,19 @@ namespace openvpn { void set_state(const int newstate) { - OPENVPN_LOG_PROTO_VERBOSE("KeyContext[" << key_id_ << "] " << (proto.is_server() ? "SERVER " : "CLIENT ") << state_string(state) << " -> " << state_string(newstate)); + OPENVPN_LOG_PROTO_VERBOSE(proto.debug_prefix() << " KeyContext[" << key_id_ << "] " << state_string(state) << " -> " << state_string(newstate)); state = newstate; } void set_event(const EventType current) { - OPENVPN_LOG_PROTO_VERBOSE("KeyContext[" << key_id_ << "] " << event_type_string(current)); + OPENVPN_LOG_PROTO_VERBOSE(proto.debug_prefix() << " KeyContext[" << key_id_ << "] " << event_type_string(current)); current_event = current; } - void set_event(const EventType next, const Time& next_time) - { - OPENVPN_LOG_PROTO_VERBOSE("KeyContext[" << key_id_ << "] " << event_type_string(next) << '(' << seconds_until(next_time) << ')'); - next_event = next; - next_event_time = next_time; - } - void set_event(const EventType current, const EventType next, const Time& next_time) { - OPENVPN_LOG_PROTO_VERBOSE("KeyContext[" << key_id_ << "] " << event_type_string(current) << " -> " << event_type_string(next) << '(' << seconds_until(next_time) << ')'); + OPENVPN_LOG_PROTO_VERBOSE(proto.debug_prefix() << " KeyContext[" << key_id_ << "] " << event_type_string(current) << " -> " << event_type_string(next) << '(' << seconds_until(next_time) << ')'); current_event = current; next_event = next; next_event_time = next_time; @@ -1559,31 +1696,76 @@ namespace openvpn { void invalidate_callback() // called by ProtoStackBase when session is invalidated { reached_active_time_ = Time(); - set_event(KEV_NONE, Time::infinite()); + next_event = KEV_NONE; + next_event_time = Time::infinite(); } - // Trigger a new SSL/TLS negotiation if packet ID (a 32-bit unsigned int) - // is getting close to wrapping around. If it wraps back to 0 without - // a renegotiation, it would cause the relay protection logic to wrongly - // think that all further packets are replays. - void test_pid_wrap(const bool pid_wrap) + // Trigger a renegotiation based on data flow condition such + // as per-key data limit or packet ID approaching wraparound. + void schedule_key_limit_renegotiation() { - if (pid_wrap && !handled_pid_wrap) + if (!key_limit_renegotiation_fired && state >= ACTIVE && !invalidated()) { - trigger_renegotiation(); - handled_pid_wrap = true; + OPENVPN_LOG_PROTO_VERBOSE(proto.debug_prefix() << " SCHEDULE KEY LIMIT RENEGOTIATION"); + + key_limit_renegotiation_fired = true; + proto.stats->error(Error::N_KEY_LIMIT_RENEG); + + // If primary, renegotiate now (within a second or two). + // If secondary, queue the renegotiation request until + // key reaches primary. + if (next_event == KEV_BECOME_PRIMARY) // secondary key before transition to primary? + set_event(KEV_RENEGOTIATE_QUEUE); // reneg request crosses over to primary, doesn't wipe next_event (KEV_BECOME_PRIMARY) + else + key_limit_reneg(KEV_RENEGOTIATE, *now); } } - void trigger_renegotiation() + // Handle data-limited keys such as Blowfish and other 64-bit block-size ciphers. + void data_limit_add(const DataLimit::Mode mode, const size_t size) { - if (state >= ACTIVE && !invalidated()) - set_event(KEV_RENEGOTIATE, KEV_EXPIRE, construct_time + proto.config->expire); + const DataLimit::State state = data_limit->add(mode, size); + if (state > DataLimit::None) + data_limit_event(mode, state); + } + + // Handle a DataLimit event. + void data_limit_event(const DataLimit::Mode mode, const DataLimit::State state) + { + OPENVPN_LOG_PROTO_VERBOSE(proto.debug_prefix() << " DATA LIMIT " << DataLimit::mode_str(mode) << ' ' << DataLimit::state_str(state) << " key_id=" << key_id_); + + // State values: + // DataLimit::Green -- first packet received and decrypted. + // DataLimit::Red -- data limit has been exceeded, so trigger a renegotiation. + if (state == DataLimit::Red) + schedule_key_limit_renegotiation(); + + // When we are in KEV_PRIMARY_PENDING state, we must receive at least + // one packet from the peer on this key before we transition to + // KEV_BECOME_PRIMARY so we can transmit on it. + if (next_event == KEV_PRIMARY_PENDING && data_limit->is_decrypt_green()) + set_event(KEV_NONE, KEV_BECOME_PRIMARY, *now + Time::Duration::seconds(1)); + } + + // Should we enter KEV_PRIMARY_PENDING state? Do it if: + // 1. we are a client, + // 2. data limit is enabled, + // 3. this is a renegotiated key in secondary context, i.e. not the first key, and + // 4. no data received yet from peer on this key. + bool data_limit_defer() const + { + return !proto.is_server() && data_limit && key_id_ && !data_limit->is_decrypt_green(); + } + + // General expiration set when key hits data limit threshold. + Time data_limit_expire() const + { + return *now + (proto.config->handshake_window * 2); } void active_event() { - set_event(KEV_ACTIVE, KEV_BECOME_PRIMARY, construct_time + proto.config->become_primary); + set_event(KEV_ACTIVE, KEV_BECOME_PRIMARY, reached_active() + proto.config->become_primary); } void process_next_event() @@ -1592,26 +1774,24 @@ namespace openvpn { { switch (next_event) { - case KEV_NEGOTIATE: - if (state >= ACTIVE) - set_event(KEV_NEGOTIATE, KEV_BECOME_PRIMARY, construct_time + proto.config->become_primary); - else - { - proto.stats->error(Error::KEV_NEGOTIATE_ERROR); - invalidate(Error::KEV_NEGOTIATE_ERROR); - set_event(KEV_NEGOTIATE_FAILED); - } - break; case KEV_BECOME_PRIMARY: - set_event(KEV_BECOME_PRIMARY, KEV_RENEGOTIATE, construct_time + proto.config->renegotiate); + if (data_limit_defer()) + set_event(KEV_NONE, KEV_PRIMARY_PENDING, data_limit_expire()); + else + set_event(KEV_BECOME_PRIMARY, KEV_RENEGOTIATE, construct_time + proto.config->renegotiate); break; case KEV_RENEGOTIATE: - set_event(KEV_RENEGOTIATE, KEV_EXPIRE, construct_time + proto.config->expire); + case KEV_RENEGOTIATE_FORCE: + prepare_expire(next_event); + break; + case KEV_NEGOTIATE: + kev_error(KEV_NEGOTIATE, Error::KEV_NEGOTIATE_ERROR); + break; + case KEV_PRIMARY_PENDING: + kev_error(KEV_PRIMARY_PENDING, Error::KEV_PENDING_ERROR); break; case KEV_EXPIRE: - proto.stats->error(Error::KEV_EXPIRE_ERROR); - invalidate(Error::KEV_EXPIRE_ERROR); - set_event(KEV_EXPIRE); + kev_error(KEV_EXPIRE, Error::N_KEV_EXPIRE); break; default: break; @@ -1619,6 +1799,13 @@ namespace openvpn { } } + void kev_error(const EventType ev, const Error::Type reason) + { + proto.stats->error(reason); + invalidate(reason); + set_event(ev); + } + unsigned int initial_op(const bool sender) const { if (key_id_) @@ -1803,7 +1990,7 @@ namespace openvpn { { std::unique_ptr dck(new DataChannelKey()); tlsprf->generate_key_expansion(dck->key, proto.psid_self, proto.psid_peer); - OPENVPN_LOG_PROTO_VERBOSE("KEY " << proto.mode().str() << ' ' << dck->key.render()); + OPENVPN_LOG_PROTO_VERBOSE(proto.debug_prefix() << " KEY " << proto.mode().str() << ' ' << dck->key.render()); tlsprf->erase(); dck.swap(data_channel_key); if (!proto.dc_deferred) @@ -2060,30 +2247,6 @@ namespace openvpn { gen_head(ACK_V1, buf); } - // for debugging - static const char *event_type_string(const EventType et) - { - switch (et) - { - case KEV_NONE: - return "KEV_NONE"; - case KEV_ACTIVE: - return "KEV_ACTIVE"; - case KEV_NEGOTIATE: - return "KEV_NEGOTIATE"; - case KEV_BECOME_PRIMARY: - return "KEV_BECOME_PRIMARY"; - case KEV_RENEGOTIATE: - return "KEV_RENEGOTIATE"; - case KEV_EXPIRE: - return "KEV_EXPIRE"; - case KEV_NEGOTIATE_FAILED: - return "KEV_NEGOTIATE_FAILED"; - default: - return "KEV_?"; - } - } - // for debugging static const char *state_string(const int s) { @@ -2135,7 +2298,7 @@ namespace openvpn { int remote_peer_id; // -1 to disable bool enable_op32; bool dirty; - bool handled_pid_wrap; + bool key_limit_renegotiation_fired; bool is_reliable; Compress::Ptr compress; CryptoDCInstance::Ptr crypto; @@ -2148,6 +2311,7 @@ namespace openvpn { std::deque app_pre_write_queue; std::unique_ptr data_channel_key; BufferComposed app_recv_buf; + std::unique_ptr data_limit; }; public: @@ -2244,9 +2408,6 @@ namespace openvpn { // validate options c.validate_complete(); - // by default, fast_transition is turned off - fast_transition = false; - // defer data channel initialization until after client options pull? dc_deferred = c.dc_deferred; @@ -2292,7 +2453,7 @@ namespace openvpn { // initialize key contexts primary.reset(new KeyContext(*this, is_client())); - OPENVPN_LOG_PROTO_VERBOSE("New KeyContext PRIMARY id=" << primary->key_id()); + OPENVPN_LOG_PROTO_VERBOSE(debug_prefix() << " New KeyContext PRIMARY id=" << primary->key_id()); // initialize keepalive timers keepalive_expire = Time::infinite(); // initially disabled @@ -2325,8 +2486,7 @@ namespace openvpn { void renegotiate() { // initialize secondary key context - secondary.reset(new KeyContext(*this, true)); - OPENVPN_LOG_PROTO_VERBOSE("New KeyContext SECONDARY id=" << secondary->key_id() << " local-triggered"); + new_secondary_key(true); secondary->start(); } @@ -2424,6 +2584,7 @@ namespace openvpn { // encrypt a data channel packet using primary KeyContext void data_encrypt(BufferAllocated& in_out) { + //OPENVPN_LOG_PROTO_VERBOSE(debug_prefix() << " DATA ENCRYPT size=" << in_out.size()); primary->encrypt(in_out); } @@ -2433,6 +2594,8 @@ namespace openvpn { { bool ret = false; + //OPENVPN_LOG_PROTO_VERBOSE(debug_prefix() << " DATA DECRYPT key_id=" << select_key_context(type, false).key_id() << " size=" << in_out.size()); + select_key_context(type, false).decrypt(in_out); // update time of most recent packet received @@ -2488,21 +2651,6 @@ namespace openvpn { // reason for invalidation if invalidated() above returns true Error::Type invalidation_reason() const { return primary->invalidation_reason(); } - // Enable original OpenVPN behavior of switching to new SSL/TLS key - // context for control channel sends immediately after renegotiation - // reaches ACTIVE state. By default, we will transition to new - // key context over the "become_primary" duration in Config. - // The default behavior is known to be more reliable, but the - // original behavior may be enabled by calling this method. - void enable_fast_transition() { fast_transition = true; } - - // A placeholder for enabling strict OpenVPN 2.x protocol - // compatibility. - void enable_strict_openvpn_2x() - { - enable_fast_transition(); - } - // Do late initialization of data channel, for example // on client after server push, or on server after client // capabilities are known. @@ -2551,6 +2699,17 @@ namespace openvpn { keepalive_parms_modified(); } + // Notify our component KeyContext when per-key Data Limits have been reached + void data_limit_notify(const int key_id, + const DataLimit::Mode cdl_mode, + const DataLimit::State cdl_status) + { + if (key_id == primary->key_id()) + primary->data_limit_notify(cdl_mode, cdl_status); + else if (secondary && key_id == secondary->key_id()) + secondary->data_limit_notify(cdl_mode, cdl_status); + } + // access the data channel settings CryptoDCSettings& dc_settings() { @@ -2651,8 +2810,7 @@ namespace openvpn { { if (KeyContext::validate(pkt.buffer(), *this, now_)) { - secondary.reset(new KeyContext(*this, false)); - OPENVPN_LOG_PROTO_VERBOSE("New KeyContext SECONDARY id=" << secondary->key_id() << " remote-triggered"); + new_secondary_key(false); return true; } else @@ -2681,26 +2839,15 @@ namespace openvpn { } // Select a KeyContext (primary or secondary) for control channel sends. - // NOTE: possible incompatibility with existing OpenVPN protocol. // Even after new key context goes active, we still wait for - // KEV_BECOME_PRIMARY event before we use it for app-level control-channel - // transmissions. Simulations have found this method to be more reliable. - // To revert to old OpenVPN behavior, call enable_fast_transition() method. + // KEV_BECOME_PRIMARY event (controlled by the become_primary duration + // in Config) before we use it for app-level control-channel + // transmissions. Simulations have found this method to be more reliable + // than the immediate rollover practiced by OpenVPN 2.x. KeyContext& select_control_send_context() { - if (!fast_transition || !secondary) - { - return *primary; - } - else - { - const Time p = primary->reached_active(); - const Time s = secondary->reached_active(); - if (p.defined() && s.defined() && s > p) - return *secondary; - else - return *primary; - } + OPENVPN_LOG_PROTO_VERBOSE(debug_prefix() << " CONTROL SEND"); + return *primary; } // Possibly send a keepalive message, and check for expiration @@ -2746,6 +2893,17 @@ namespace openvpn { return did_work; } + // Create a new secondary key. + // initiator -- + // false : remote renegotiation request + // true : local renegotiation request + void new_secondary_key(const bool initiator) + { + // Create the secondary + secondary.reset(new KeyContext(*this, initiator)); + OPENVPN_LOG_PROTO_VERBOSE(debug_prefix() << " New KeyContext SECONDARY id=" << secondary->key_id() << (initiator ? " local-triggered" : " remote-triggered")); + } + // Promote a newly renegotiated KeyContext to primary status. // This is usually triggered by become_primary variable (Time::Duration) // in Config. @@ -2754,7 +2912,7 @@ namespace openvpn { primary.swap(secondary); primary->rekey(CryptoDCInstance::PROMOTE_SECONDARY_TO_PRIMARY); secondary->prepare_expire(); - OPENVPN_LOG_PROTO_VERBOSE("*** PROMOTE_SECONDARY_TO_PRIMARY pri=" << primary->key_id() << " sec=" << secondary->key_id()); + OPENVPN_LOG_PROTO_VERBOSE(debug_prefix() << " PROMOTE_SECONDARY_TO_PRIMARY"); } void process_primary_event() @@ -2766,11 +2924,12 @@ namespace openvpn { switch (ev) { case KeyContext::KEV_ACTIVE: - OPENVPN_LOG_PROTO_VERBOSE("*** SESSION_ACTIVE"); + OPENVPN_LOG_PROTO_VERBOSE(debug_prefix() << " SESSION_ACTIVE"); primary->rekey(CryptoDCInstance::ACTIVATE_PRIMARY); active(); break; case KeyContext::KEV_RENEGOTIATE: + case KeyContext::KEV_RENEGOTIATE_FORCE: renegotiate(); break; case KeyContext::KEV_EXPIRE: @@ -2782,7 +2941,7 @@ namespace openvpn { disconnect(Error::PRIMARY_EXPIRE); // primary context expired and no secondary context available } break; - case KeyContext::KEV_NEGOTIATE_FAILED: + case KeyContext::KEV_NEGOTIATE: stats->error(Error::HANDSHAKE_TIMEOUT); disconnect(Error::HANDSHAKE_TIMEOUT); // primary negotiation failed break; @@ -2790,6 +2949,7 @@ namespace openvpn { break; } } + primary->set_next_event_if_unspecified(); } void process_secondary_event() @@ -2812,16 +2972,40 @@ namespace openvpn { secondary->rekey(CryptoDCInstance::DEACTIVATE_SECONDARY); secondary.reset(); break; - case KeyContext::KEV_NEGOTIATE_FAILED: + case KeyContext::KEV_RENEGOTIATE_QUEUE: + primary->key_limit_reneg(KeyContext::KEV_RENEGOTIATE_FORCE, secondary->become_primary_time()); + break; + case KeyContext::KEV_NEGOTIATE: stats->error(Error::HANDSHAKE_TIMEOUT); + case KeyContext::KEV_PRIMARY_PENDING: + case KeyContext::KEV_RENEGOTIATE_FORCE: renegotiate(); break; default: break; } } + if (secondary) + secondary->set_next_event_if_unspecified(); } +#ifdef OPENVPN_INSTRUMENTATION + std::string debug_prefix() + { + std::string ret = openvpn::to_string(now_->raw()); + ret += is_server() ? " SERVER[" : " CLIENT["; + if (primary) + ret += openvpn::to_string(primary->key_id()); + if (secondary) + { + ret += '/'; + ret += openvpn::to_string(secondary->key_id()); + } + ret += ']'; + return ret; + } +#endif + // key_id starts at 0, increments to KEY_ID_MASK, then recycles back to 1. // Therefore, if key_id is 0, it is the first key. unsigned int next_key_id() @@ -2876,8 +3060,6 @@ namespace openvpn { KeyContext::Ptr secondary; bool dc_deferred; - bool fast_transition; - // END ProtoContext data members }; diff --git a/openvpn/ssl/protostack.hpp b/openvpn/ssl/protostack.hpp index b8a5648a..e5214fa9 100644 --- a/openvpn/ssl/protostack.hpp +++ b/openvpn/ssl/protostack.hpp @@ -91,11 +91,13 @@ namespace openvpn { ProtoStackBase(SSLFactoryAPI& ssl_factory, // SSL factory object that can be used to generate new SSL sessions TimePtr now_arg, // pointer to current time + const Time::Duration& tls_timeout_arg, // packet retransmit timeout const Frame::Ptr& frame, // contains info on how to allocate and align buffers const SessionStats::Ptr& stats_arg, // error statistics const id_t span, // basically the window size for our reliability layer const size_t max_ack_list) // maximum number of ACK messages to bundle in one packet - : ssl_(ssl_factory.ssl()), + : tls_timeout(tls_timeout_arg), + ssl_(ssl_factory.ssl()), frame_(frame), up_stack_reentry_level(0), invalidated_(false), @@ -190,7 +192,7 @@ namespace openvpn { if (m.ready_retransmit(*now)) { parent().net_send(m.packet, NET_SEND_RETRANSMIT); - m.reset_retransmit(*now); + m.reset_retransmit(*now, tls_timeout); } } update_retransmit(); @@ -326,7 +328,7 @@ namespace openvpn { // encapsulate SSL ciphertext packets while (ssl_->read_ciphertext_ready() && rel_send.ready()) { - typename ReliableSend::Message& m = rel_send.send(*now); + typename ReliableSend::Message& m = rel_send.send(*now, tls_timeout); m.packet = PACKET(ssl_->read_ciphertext()); // encapsulate packet @@ -350,7 +352,7 @@ namespace openvpn { { while (!raw_write_queue.empty() && rel_send.ready()) { - typename ReliableSend::Message& m = rel_send.send(*now); + typename ReliableSend::Message& m = rel_send.send(*now, tls_timeout); m.packet = raw_write_queue.front(); raw_write_queue.pop_front(); @@ -453,6 +455,7 @@ namespace openvpn { } private: + const Time::Duration tls_timeout; typename SSLAPI::Ptr ssl_; Frame::Ptr frame_; int up_stack_reentry_level; diff --git a/openvpn/time/durhelper.hpp b/openvpn/time/durhelper.hpp index 9664b067..d77d689d 100644 --- a/openvpn/time/durhelper.hpp +++ b/openvpn/time/durhelper.hpp @@ -28,45 +28,62 @@ namespace openvpn { inline void set_duration_parm(Time::Duration& dur, - const char *name, + const std::string& name, const std::string& valstr, const unsigned int min_value, - const bool x2) + const bool x2, // multiply result by 2 + const bool ms) // values are in milliseconds rather than seconds { - const unsigned int maxdur = 60*60*24*7; // maximum duration -- 7 days + const unsigned int maxdur = ms ? 1000*60*60*24 : 60*60*24*7; // maximum duration -- milliseconds: 1 day, seconds: 7 days unsigned int value = 0; const bool status = parse_number(valstr, value); if (!status) - OPENVPN_THROW(option_error, name << ": error parsing number of seconds"); + OPENVPN_THROW(option_error, name << ": error parsing number of " << (ms ? "milliseconds" : "seconds")); if (x2) value *= 2; if (value == 0 || value > maxdur) value = maxdur; if (value < min_value) value = min_value; - dur = Time::Duration::seconds(value); + dur = ms ? Time::Duration::milliseconds(value) : Time::Duration::seconds(value); } inline const Option* load_duration_parm(Time::Duration& dur, - const char *name, + const std::string& name, const OptionList& opt, const unsigned int min_value, - const bool x2) + const bool x2, + const bool allow_ms) { - const Option *o = opt.get_ptr(name); - if (o) - set_duration_parm(dur, name, o->get(1, 16), min_value, x2); - return o; + // look for milliseconds given as -ms + if (allow_ms) + { + const Option *o = opt.get_ptr(name + "-ms"); + if (o) + { + set_duration_parm(dur, name, o->get(1, 16), min_value, x2, true); + return o; + } + } + + // look for seconds given as + { + const Option *o = opt.get_ptr(name); + if (o) + set_duration_parm(dur, name, o->get(1, 16), allow_ms ? 1 : min_value, x2, false); + return o; + } } - inline Time::Duration load_duration_default(const char *name, + inline Time::Duration load_duration_default(const std::string& name, const OptionList& opt, const Time::Duration& default_duration, const unsigned int min_value, - const bool x2) + const bool x2, + const bool allow_ms) { Time::Duration ret(default_duration); - load_duration_parm(ret, name, opt, min_value, x2); + load_duration_parm(ret, name, opt, min_value, x2, allow_ms); return ret; } diff --git a/openvpn/transport/server/transbase.hpp b/openvpn/transport/server/transbase.hpp index a3e83caa..3459796d 100644 --- a/openvpn/transport/server/transbase.hpp +++ b/openvpn/transport/server/transbase.hpp @@ -39,6 +39,7 @@ #include #include #include +#include namespace openvpn { @@ -118,6 +119,12 @@ namespace openvpn { // client float notification virtual void float_notify(const PeerAddr::Ptr& addr) = 0; + // Data limit notification -- trigger a renegotiation + // when cdl_status == DataLimit::Red. + virtual void data_limit_notify(const int key_id, + const DataLimit::Mode cdl_mode, + const DataLimit::State cdl_status) = 0; + // push a halt or restart message to client virtual void push_halt_restart_msg(const HaltRestart::Type type, const std::string& reason, diff --git a/test/ovpncli/go b/test/ovpncli/go index e6cef3e0..2a00d9ad 100755 --- a/test/ovpncli/go +++ b/test/ovpncli/go @@ -5,6 +5,7 @@ GCC_EXTRA="$GCC_EXTRA -DOPENVPN_SHOW_SESSION_TOKEN" [ "$EXIT" = "1" ] && GCC_EXTRA="$GCC_EXTRA -DTUN_NULL_EXIT" [ "$GREMLIN" = "1" ] && GCC_EXTRA="$GCC_EXTRA -DOPENVPN_GREMLIN" [ "$DEX" = "1" ] && GCC_EXTRA="$GCC_EXTRA -DOPENVPN_DISABLE_EXPLICIT_EXIT" +[ "$BS64" = "1" ] && GCC_EXTRA="$GCC_EXTRA -DOPENVPN_BS64_DATA_LIMIT=2500000" if [ "$AGENT" = "1" ]; then GCC_EXTRA="$GCC_EXTRA -DOPENVPN_COMMAND_AGENT" export JSON=1 diff --git a/test/ssl/proto.cpp b/test/ssl/proto.cpp index 31a57340..7cd3a84d 100644 --- a/test/ssl/proto.cpp +++ b/test/ssl/proto.cpp @@ -39,6 +39,51 @@ #define OPENVPN_DEBUG #define OPENVPN_ENABLE_ASSERT #define USE_TLS_AUTH +#define OPENVPN_INSTRUMENTATION + +// Data limits for Blowfish and other 64-bit block-size ciphers +#ifndef BF +#define BF 0 +#endif +#define OPENVPN_BS64_DATA_LIMIT 50000 +#if BF == 1 +#define PROTO_CIPHER "BF-CBC" +#define TLS_VER_MIN TLSVersion::UNDEF +#define HANDSHAKE_WINDOW 60 +#define BECOME_PRIMARY_CLIENT 5 +#define BECOME_PRIMARY_SERVER 5 +#define TLS_TIMEOUT_CLIENT 1000 +#define TLS_TIMEOUT_SERVER 1000 +#define FEEDBACK 0 +#elif BF == 2 +#define PROTO_CIPHER "BF-CBC" +#define TLS_VER_MIN TLSVersion::UNDEF +#define HANDSHAKE_WINDOW 10 +#define BECOME_PRIMARY_CLIENT 10 +#define BECOME_PRIMARY_SERVER 10 +#define TLS_TIMEOUT_CLIENT 2000 +#define TLS_TIMEOUT_SERVER 1000 +#define FEEDBACK 0 +#elif BF == 3 +#define PROTO_CIPHER "BF-CBC" +#define TLS_VER_MIN TLSVersion::UNDEF +#define HANDSHAKE_WINDOW 60 +#define BECOME_PRIMARY_CLIENT 60 +#define BECOME_PRIMARY_SERVER 10 +#define TLS_TIMEOUT_CLIENT 2000 +#define TLS_TIMEOUT_SERVER 1000 +#define FEEDBACK 0 +#elif BF != 0 +#error unknown BF value +#endif + +// TLS timeout +#ifndef TLS_TIMEOUT_CLIENT +#define TLS_TIMEOUT_CLIENT 2000 +#endif +#ifndef TLS_TIMEOUT_SERVER +#define TLS_TIMEOUT_SERVER 2000 +#endif // NoisyWire #ifndef NOERR @@ -52,6 +97,13 @@ #define RENEG 900 #endif +// feedback +#ifndef FEEDBACK +#define FEEDBACK 1 +#else +#define FEEDBACK 0 +#endif + // number of threads to use for test #ifndef N_THREADS #define N_THREADS 1 @@ -87,17 +139,17 @@ // setup cipher #ifndef PROTO_CIPHER #ifdef PROTOv2 -#define PROTO_CIPHER AES-256-GCM +#define PROTO_CIPHER "AES-256-GCM" #define TLS_VER_MIN TLSVersion::V1_2 #else -#define PROTO_CIPHER AES-128-CBC +#define PROTO_CIPHER "AES-128-CBC" #define TLS_VER_MIN TLSVersion::UNDEF #endif #endif // setup digest #ifndef PROTO_DIGEST -#define PROTO_DIGEST SHA1 +#define PROTO_DIGEST "SHA1" #endif // setup compressor @@ -272,7 +324,7 @@ public: #if defined(VERBOSE) || defined(DROUGHT_LIMIT) { const unsigned int r = drought.raw(); -#ifdef VERBOSE +#if defined(VERBOSE) std::cout << "*** Drought " << name << " has reached " << r << std::endl; #endif #ifdef DROUGHT_LIMIT @@ -309,15 +361,14 @@ public: typedef Base::PacketType PacketType; + OPENVPN_EXCEPTION(session_invalidated); + TestProto(const Base::Config::Ptr& config, const SessionStats::Ptr& stats) : Base(config, stats), control_drought("control", config->now), data_drought("data", config->now), - frame(config->frame), - app_bytes_(0), - net_bytes_(0), - data_bytes_(0) + frame(config->frame) { // zero progress value std::memset(progress_, 0, 11); @@ -332,7 +383,6 @@ public: void initial_app_send(const char *msg) { Base::start(); - const size_t msglen = std::strlen(msg) + 1; BufferAllocated app_buf((unsigned char *)msg, msglen, 0); copy_progress(app_buf); @@ -340,6 +390,28 @@ public: flush(true); } + void app_send_templ_init(const char *msg) + { + Base::start(); + const size_t msglen = std::strlen(msg) + 1; + templ.reset(new BufferAllocated((unsigned char *)msg, msglen, 0)); + flush(true); + } + + void app_send_templ() + { +#if !FEEDBACK + if (bool(iteration++ & 1) == is_server()) + { + modmsg(templ); + BufferAllocated app_buf(*templ); + control_send(std::move(app_buf)); + flush(true); + ++n_control_send_; + } +#endif + } + bool do_housekeeping() { if (now() >= Base::next_housekeeping()) @@ -390,6 +462,8 @@ public: size_t net_bytes() const { return net_bytes_; } size_t app_bytes() const { return app_bytes_; } size_t data_bytes() const { return data_bytes_; } + size_t n_control_recv() const { return n_control_recv_; } + size_t n_control_send() const { return n_control_send_; } const char *progress() const { return progress_; } @@ -399,6 +473,12 @@ public: control_drought.event(); } + void check_invalidated() + { + if (Base::invalidated()) + throw session_invalidated(Error::name(Base::invalidation_reason())); + } + std::deque net_out; DroughtMeasure control_drought; @@ -425,9 +505,12 @@ private: std::cout << now().raw() << " " << mode().str() << " " << show << std::endl; } #endif +#if FEEDBACK modmsg(work); control_send(std::move(work)); +#endif control_drought.event(); + ++n_control_recv_; } void copy_progress(Buffer& buf) @@ -464,9 +547,13 @@ private: } Frame::Ptr frame; - size_t app_bytes_; - size_t net_bytes_; - size_t data_bytes_; + size_t app_bytes_ = 0; + size_t net_bytes_ = 0; + size_t data_bytes_ = 0; + size_t n_control_send_ = 0; + size_t n_control_recv_ = 0; + BufferPtr templ; + size_t iteration = 0; char progress_[11]; }; @@ -522,8 +609,6 @@ private: class NoisyWire { public: - OPENVPN_SIMPLE_EXCEPTION(session_invalidated); - NoisyWire(const std::string title_arg, TimePtr now_arg, RandomAPI& rand_arg, @@ -543,8 +628,8 @@ public: void xfer(T1& a, T2& b) { // check for errors - if (a.invalidated() || b.invalidated()) - throw session_invalidated(); + a.check_invalidated(); + b.check_invalidated(); // need to retransmit? if (a.do_housekeeping()) @@ -554,10 +639,13 @@ public: #endif } + // queue a control channel packet + a.app_send_templ(); + // queue a data channel packet if (a.data_channel_ready()) { - BufferPtr bp = a.data_encrypt_string("Waiting for godot..."); + BufferPtr bp = a.data_encrypt_string("Waiting for godot A... Waiting for godot B... Waiting for godot C... Waiting for godot D... Waiting for godot E... Waiting for godot F... Waiting for godot G... Waiting for godot H... Waiting for godot I... Waiting for godot J..."); wire.push_back(bp); } @@ -594,7 +682,7 @@ public: #ifdef VERBOSE if (bp->size()) { - const std::string show((char *)bp->data(), bp->size()); + const std::string show((char *)bp->data(), std::min(bp->size(), size_t(40))); std::cout << now->raw() << " " << title << " DATA CHANNEL DECRYPT: " << show << std::endl; } #endif @@ -606,6 +694,13 @@ public: #endif } } + else + { +#ifdef VERBOSE + std::cout << now->raw() << " " << title << " KEY_STATE_ERROR" << std::endl; +#endif + b.stat().error(Error::KEY_STATE_ERROR); + } } b.flush(true); } @@ -786,12 +881,12 @@ int test(const int thread_num) cp->remote_peer_id = 100; #endif cp->comp_ctx = CompressContext(COMP_METH, false); - cp->dc.set_cipher(CryptoAlgs::lookup(STRINGIZE(PROTO_CIPHER))); - cp->dc.set_digest(CryptoAlgs::lookup(STRINGIZE(PROTO_DIGEST))); + cp->dc.set_cipher(CryptoAlgs::lookup(PROTO_CIPHER)); + cp->dc.set_digest(CryptoAlgs::lookup(PROTO_DIGEST)); #ifdef USE_TLS_AUTH cp->tls_auth_factory.reset(new CryptoOvpnHMACFactory()); cp->tls_auth_key.parse(tls_auth_key); - cp->set_tls_auth_digest(CryptoAlgs::lookup(STRINGIZE(PROTO_DIGEST))); + cp->set_tls_auth_digest(CryptoAlgs::lookup(PROTO_DIGEST)); cp->key_direction = 0; #endif cp->reliable_window = 4; @@ -804,7 +899,12 @@ int test(const int thread_num) #else cp->handshake_window = Time::Duration::seconds(18); // will cause a small number of handshake failures #endif - cp->become_primary = Time::Duration::seconds(30); +#ifdef BECOME_PRIMARY_CLIENT + cp->become_primary = Time::Duration::seconds(BECOME_PRIMARY_CLIENT); +#else + cp->become_primary = cp->handshake_window; +#endif + cp->tls_timeout = Time::Duration::milliseconds(TLS_TIMEOUT_CLIENT); #if defined(CLIENT_NO_RENEG) cp->renegotiate = Time::Duration::infinite(); #else @@ -851,12 +951,12 @@ int test(const int thread_num) sp->remote_peer_id = 101; #endif sp->comp_ctx = CompressContext(COMP_METH, false); - sp->dc.set_cipher(CryptoAlgs::lookup(STRINGIZE(PROTO_CIPHER))); - sp->dc.set_digest(CryptoAlgs::lookup(STRINGIZE(PROTO_DIGEST))); + sp->dc.set_cipher(CryptoAlgs::lookup(PROTO_CIPHER)); + sp->dc.set_digest(CryptoAlgs::lookup(PROTO_DIGEST)); #ifdef USE_TLS_AUTH sp->tls_auth_factory.reset(new CryptoOvpnHMACFactory()); sp->tls_auth_key.parse(tls_auth_key); - sp->set_tls_auth_digest(CryptoAlgs::lookup(STRINGIZE(PROTO_DIGEST))); + sp->set_tls_auth_digest(CryptoAlgs::lookup(PROTO_DIGEST)); sp->key_direction = 1; #endif sp->reliable_window = 4; @@ -869,7 +969,12 @@ int test(const int thread_num) #else sp->handshake_window = Time::Duration::seconds(17) + Time::Duration::binary_ms(512); #endif - sp->become_primary = Time::Duration::seconds(30); +#ifdef BECOME_PRIMARY_SERVER + sp->become_primary = Time::Duration::seconds(BECOME_PRIMARY_SERVER); +#else + sp->become_primary = sp->handshake_window; +#endif + sp->tls_timeout = Time::Duration::milliseconds(TLS_TIMEOUT_SERVER); #if defined(SERVER_NO_RENEG) sp->renegotiate = Time::Duration::infinite(); #else @@ -907,9 +1012,14 @@ int test(const int thread_num) int j = -1; try { +#if FEEDBACK // start feedback loop cli_proto.initial_app_send(message); serv_proto.start(); +#else + cli_proto.app_send_templ_init(message); + serv_proto.app_send_templ_init(message); +#endif // message loop for (j = 0; j < ITER; ++j) @@ -937,6 +1047,9 @@ int test(const int thread_num) << " net_bytes=" << nb << " data_bytes=" << db << " prog=" << cli_proto.progress() << '/' << serv_proto.progress() +#if !FEEDBACK + << " CTRL=" << cli_proto.n_control_recv() << '/' << cli_proto.n_control_send() << '/' << serv_proto.n_control_recv() << '/' << serv_proto.n_control_send() +#endif << " D=" << cli_proto.control_drought().raw() << '/' << cli_proto.data_drought().raw() << '/' << serv_proto.control_drought().raw() << '/' << serv_proto.data_drought().raw() << " N=" << cli_proto.negotiations() << '/' << serv_proto.negotiations() << " SH=" << cli_proto.slowest_handshake().raw() << '/' << serv_proto.slowest_handshake().raw() @@ -948,6 +1061,10 @@ int test(const int thread_num) cli_stats->show_error_counts(); std::cerr << "-------- SERVER STATS --------" << std::endl; serv_stats->show_error_counts(); +#endif +#ifdef OPENVPN_MAX_DATALIMIT_BYTES + std::cerr << "------------------------------" << std::endl; + std::cerr << "MAX_DATALIMIT_BYTES=" << DataLimit::max_bytes() << std::endl; #endif } catch (const std::exception& e)