Always include ACKs for the last seen control packets

This adds an MRU cache for the last seen packets from the peer to send acks
to all recently recently  packets. This allows packets to be acknowledged
even if a single P_ACK_V1 gets lost, avoiding retransmissions. The downside
is that we add up to 28 byte to an P_ACK_V1 (7* packet_id) and up to 24
bytes to other control channel packets (4* packet_id + peer session id).
However these small increases in packet size are a small price to pay for
increased reliability.

Currently OpenVPN will only send the absolute minimum of ACK messages. A
single lost ACK message will trigger a resend from the peer and another
ACK message.

Patch v2: fix multiple typos/grammar. Change lru to mru (this is really an
          MRU cache), add more unit test cases

Signed-off-by: Arne Schwabe <arne@rfc2549.org>
Acked-by: Frank Lichtenheld <frank@lichtenheld.com>
Message-Id: <20220831134140.913337-1-arne@rfc2549.org>
URL: https://www.mail-archive.com/openvpn-devel@lists.sourceforge.net/msg25143.html
Signed-off-by: Gert Doering <gert@greenie.muc.de>
This commit is contained in:
Arne Schwabe 2022-08-31 15:41:39 +02:00 committed by Gert Doering
parent 5f6ea59759
commit c879609534
6 changed files with 155 additions and 8 deletions

View File

@ -206,10 +206,56 @@ reliable_ack_parse(struct buffer *buf, struct reliable_ack *ack,
return true; return true;
} }
/**
* Copies the first n acks from \c ack to \c ack_mru
*/
void
copy_acks_to_mru(struct reliable_ack *ack, struct reliable_ack *ack_mru, int n)
{
ASSERT(ack->len >= n);
/* This loop is backward to ensure the same order as in ack */
for (int i = n-1; i >= 0; i--)
{
packet_id_type id = ack->packet_id[i];
/* Handle special case of ack_mru empty */
if (ack_mru->len == 0)
{
ack_mru->len = 1;
ack_mru->packet_id[0] = id;
}
bool idfound = false;
/* Move all existing entries one to the right */
packet_id_type move = id;
for (int j = 0; j < ack_mru->len; j++)
{
packet_id_type tmp = ack_mru->packet_id[j];
ack_mru->packet_id[j] = move;
move = tmp;
if (move == id)
{
idfound = true;
break;
}
}
if (!idfound && ack_mru->len < RELIABLE_ACK_SIZE)
{
ack_mru->packet_id[ack_mru->len] = move;
ack_mru->len++;
}
}
}
/* write a packet ID acknowledgement record to buf, */ /* write a packet ID acknowledgement record to buf, */
/* removing all acknowledged entries from ack */ /* removing all acknowledged entries from ack */
bool bool
reliable_ack_write(struct reliable_ack *ack, reliable_ack_write(struct reliable_ack *ack,
struct reliable_ack *ack_mru,
struct buffer *buf, struct buffer *buf,
const struct session_id *sid, int max, bool prepend) const struct session_id *sid, int max, bool prepend)
{ {
@ -222,23 +268,36 @@ reliable_ack_write(struct reliable_ack *ack,
{ {
n = max; n = max;
} }
sub = buf_sub(buf, ACK_SIZE(n), prepend);
copy_acks_to_mru(ack, ack_mru, n);
/* Number of acks we can resend that still fit into the packet */
uint8_t total_acks = min_int(max, ack_mru->len);
sub = buf_sub(buf, ACK_SIZE(total_acks), prepend);
if (!BDEF(&sub)) if (!BDEF(&sub))
{ {
goto error; goto error;
} }
ASSERT(buf_write(&sub, &n, sizeof(n))); ASSERT(buf_write_u8(&sub, total_acks));
for (i = 0; i < n; ++i)
/* Write the actual acks to the packets. Since we copied the acks that
* are going out now already to the front of ack_mru we can fetch all
* acks from ack_mru */
for (i = 0; i < total_acks; ++i)
{ {
packet_id_type pid = ack->packet_id[i]; packet_id_type pid = ack_mru->packet_id[i];
packet_id_type net_pid = htonpid(pid); packet_id_type net_pid = htonpid(pid);
ASSERT(buf_write(&sub, &net_pid, sizeof(net_pid))); ASSERT(buf_write(&sub, &net_pid, sizeof(net_pid)));
dmsg(D_REL_DEBUG, "ACK write ID " packet_id_format " (ack->len=%d, n=%d)", (packet_id_print_type)pid, ack->len, n); dmsg(D_REL_DEBUG, "ACK write ID " packet_id_format " (ack->len=%d, n=%d)", (packet_id_print_type)pid, ack->len, n);
} }
if (n) if (total_acks)
{ {
ASSERT(session_id_defined(sid)); ASSERT(session_id_defined(sid));
ASSERT(session_id_write(sid, &sub)); ASSERT(session_id_write(sid, &sub));
}
if (n)
{
for (i = 0, j = n; j < ack->len; ) for (i = 0, j = n; j < ack->len; )
{ {
ack->packet_id[i++] = ack->packet_id[j++]; ack->packet_id[i++] = ack->packet_id[j++];

View File

@ -197,6 +197,9 @@ reliable_ack_outstanding(struct reliable_ack *ack)
* *
* @param ack The acknowledgment structure containing packet IDs to be * @param ack The acknowledgment structure containing packet IDs to be
* acknowledged. * acknowledged.
* @param ack_mru List of packets we have acknowledged before. Packets from
* \c ack will be moved here and if there is space in our
* ack structure we will fill it with packets from this
* @param buf The buffer into which the acknowledgment record will be * @param buf The buffer into which the acknowledgment record will be
* written. * written.
* @param sid The session ID of the VPN tunnel associated with the * @param sid The session ID of the VPN tunnel associated with the
@ -211,6 +214,7 @@ reliable_ack_outstanding(struct reliable_ack *ack)
* @li False, if an error occurs during processing. * @li False, if an error occurs during processing.
*/ */
bool reliable_ack_write(struct reliable_ack *ack, bool reliable_ack_write(struct reliable_ack *ack,
struct reliable_ack *ack_mru,
struct buffer *buf, struct buffer *buf,
const struct session_id *sid, int max, bool prepend); const struct session_id *sid, int max, bool prepend);
@ -370,6 +374,19 @@ bool reliable_ack_acknowledge_packet_id(struct reliable_ack *ack, packet_id_type
*/ */
struct reliable_entry *reliable_get_entry_sequenced(struct reliable *rel); struct reliable_entry *reliable_get_entry_sequenced(struct reliable *rel);
/**
* Copies the first n acks from \c ack to \c ack_mru
*
* @param ack The reliable structure to copy the acks from
* @param ack_mru The reliable structure to insert the acks into
* @param n The number of ACKS to copy
*/
void
copy_acks_to_mru(struct reliable_ack *ack, struct reliable_ack *ack_mru, int n);
/** /**
* Remove an entry from a reliable structure. * Remove an entry from a reliable structure.
* *

View File

@ -357,7 +357,8 @@ calc_control_channel_frame_overhead(const struct tls_session *session)
overhead += SID_SIZE; overhead += SID_SIZE;
/* ACK array and remote SESSION ID (part of the ACK array) */ /* ACK array and remote SESSION ID (part of the ACK array) */
overhead += ACK_SIZE(min_int(reliable_ack_outstanding(ks->rec_ack), CONTROL_SEND_ACK_MAX)); int ackstosend = reliable_ack_outstanding(ks->rec_ack) + ks->lru_acks->len;
overhead += ACK_SIZE(min_int(ackstosend, CONTROL_SEND_ACK_MAX));
/* Message packet id */ /* Message packet id */
overhead += sizeof(packet_id_type); overhead += sizeof(packet_id_type);
@ -993,6 +994,7 @@ key_state_init(struct tls_session *session, struct key_state *ks)
ALLOC_OBJ_CLEAR(ks->send_reliable, struct reliable); ALLOC_OBJ_CLEAR(ks->send_reliable, struct reliable);
ALLOC_OBJ_CLEAR(ks->rec_reliable, struct reliable); ALLOC_OBJ_CLEAR(ks->rec_reliable, struct reliable);
ALLOC_OBJ_CLEAR(ks->rec_ack, struct reliable_ack); ALLOC_OBJ_CLEAR(ks->rec_ack, struct reliable_ack);
ALLOC_OBJ_CLEAR(ks->lru_acks, struct reliable_ack);
/* allocate buffers */ /* allocate buffers */
ks->plaintext_read_buf = alloc_buf(TLS_CHANNEL_BUF_SIZE); ks->plaintext_read_buf = alloc_buf(TLS_CHANNEL_BUF_SIZE);
@ -1063,6 +1065,7 @@ key_state_free(struct key_state *ks, bool clear)
reliable_free(ks->rec_reliable); reliable_free(ks->rec_reliable);
free(ks->rec_ack); free(ks->rec_ack);
free(ks->lru_acks);
free(ks->key_src); free(ks->key_src);
packet_id_free(&ks->crypto_options.packet_id); packet_id_free(&ks->crypto_options.packet_id);

View File

@ -233,6 +233,7 @@ struct key_state
struct reliable *send_reliable; /* holds a copy of outgoing packets until ACK received */ struct reliable *send_reliable; /* holds a copy of outgoing packets until ACK received */
struct reliable *rec_reliable; /* order incoming ciphertext packets before we pass to TLS */ struct reliable *rec_reliable; /* order incoming ciphertext packets before we pass to TLS */
struct reliable_ack *rec_ack; /* buffers all packet IDs we want to ACK back to sender */ struct reliable_ack *rec_ack; /* buffers all packet IDs we want to ACK back to sender */
struct reliable_ack *lru_acks; /* keeps the most recently acked packages*/
/** Holds outgoing message for the control channel until ks->state reaches /** Holds outgoing message for the control channel until ks->state reaches
* S_ACTIVE */ * S_ACTIVE */

View File

@ -179,7 +179,8 @@ write_control_auth(struct tls_session *session,
ASSERT(link_socket_actual_defined(&ks->remote_addr)); ASSERT(link_socket_actual_defined(&ks->remote_addr));
ASSERT(reliable_ack_write ASSERT(reliable_ack_write
(ks->rec_ack, buf, &ks->session_id_remote, max_ack, prepend_ack)); (ks->rec_ack, ks->lru_acks, buf, &ks->session_id_remote,
max_ack, prepend_ack));
msg(D_TLS_DEBUG, "%s(): %s", __func__, packet_opcode_name(opcode)); msg(D_TLS_DEBUG, "%s(): %s", __func__, packet_opcode_name(opcode));

View File

@ -210,6 +210,70 @@ test_get_num_output_sequenced_available(void **state)
reliable_free(rel); reliable_free(rel);
} }
static void
test_copy_acks_to_lru(void **state)
{
struct reliable_ack ack = { .len = 4, .packet_id = {2, 1, 3, 2} };
struct reliable_ack mru_ack = {0 };
/* Test copying to empty ack structure */
copy_acks_to_mru(&ack, &mru_ack, 4);
assert_int_equal(mru_ack.len, 3);
assert_int_equal(mru_ack.packet_id[0], 2);
assert_int_equal(mru_ack.packet_id[1], 1);
assert_int_equal(mru_ack.packet_id[2], 3);
/* Copying again should not change the result */
copy_acks_to_mru(&ack, &mru_ack, 4);
assert_int_equal(mru_ack.len, 3);
assert_int_equal(mru_ack.packet_id[0], 2);
assert_int_equal(mru_ack.packet_id[1], 1);
assert_int_equal(mru_ack.packet_id[2], 3);
/* Copying just the first two element should not change the order
* as they are still the most recent*/
struct reliable_ack mru_ack2 = mru_ack;
copy_acks_to_mru(&ack, &mru_ack2, 2);
assert_int_equal(mru_ack2.packet_id[0], 2);
assert_int_equal(mru_ack2.packet_id[1], 1);
assert_int_equal(mru_ack2.packet_id[2], 3);
/* Adding just two packets shoudl ignore the 42 in array and
* reorder the order in the MRU */
struct reliable_ack ack2 = { .len = 3, .packet_id = {3, 2, 42} };
copy_acks_to_mru(&ack2, &mru_ack2, 2);
assert_int_equal(mru_ack2.packet_id[0], 3);
assert_int_equal(mru_ack2.packet_id[1], 2);
assert_int_equal(mru_ack2.packet_id[2], 1);
/* Copying a zero array into it should also change nothing */
struct reliable_ack empty_ack = { .len = 0 };
copy_acks_to_mru(&empty_ack, &mru_ack, 0);
assert_int_equal(mru_ack.len, 3);
assert_int_equal(mru_ack.packet_id[0], 2);
assert_int_equal(mru_ack.packet_id[1], 1);
assert_int_equal(mru_ack.packet_id[2], 3);
/* Or should just 0 elements of the ack */
copy_acks_to_mru(&ack, &mru_ack, 0);
assert_int_equal(mru_ack.len, 3);
assert_int_equal(mru_ack.packet_id[0], 2);
assert_int_equal(mru_ack.packet_id[1], 1);
assert_int_equal(mru_ack.packet_id[2], 3);
struct reliable_ack ack3 = { .len = 7, .packet_id = {5, 6, 7, 8, 9, 10, 11}};
/* Adding multiple acks tests if the a full array is handled correctly */
copy_acks_to_mru(&ack3, &mru_ack, 7);
struct reliable_ack expected_ack = { .len = 8, .packet_id = {5, 6, 7, 8, 9, 10, 11, 2}};
assert_int_equal(mru_ack.len, expected_ack.len);
assert_memory_equal(mru_ack.packet_id, expected_ack.packet_id, sizeof(expected_ack.packet_id));
}
int int
main(void) main(void)
{ {
@ -232,7 +296,9 @@ main(void)
cmocka_unit_test_setup_teardown(test_packet_id_write_long_wrap, cmocka_unit_test_setup_teardown(test_packet_id_write_long_wrap,
test_packet_id_write_setup, test_packet_id_write_setup,
test_packet_id_write_teardown), test_packet_id_write_teardown),
cmocka_unit_test(test_get_num_output_sequenced_available) cmocka_unit_test(test_get_num_output_sequenced_available),
cmocka_unit_test(test_copy_acks_to_lru)
}; };
return cmocka_run_group_tests_name("packet_id tests", tests, NULL, NULL); return cmocka_run_group_tests_name("packet_id tests", tests, NULL, NULL);