Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tls: use custom instead of socket based bio #12911

Merged
merged 7 commits into from
Sep 4, 2020
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions source/extensions/transport_sockets/tls/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,14 @@ envoy_cc_extension(

envoy_cc_library(
name = "ssl_socket_lib",
srcs = ["ssl_socket.cc"],
hdrs = ["ssl_socket.h"],
srcs = [
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can io_handle_bio.{cc,h} in a separate envoy_cc_library?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

"io_handle_bio.cc",
"ssl_socket.cc",
],
hdrs = [
"io_handle_bio.h",
"ssl_socket.h",
],
external_deps = [
"abseil_hash",
"abseil_node_hash_map",
Expand Down
140 changes: 140 additions & 0 deletions source/extensions/transport_sockets/tls/io_handle_bio.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
#include "extensions/transport_sockets/tls/io_handle_bio.h"

#include "envoy/buffer/buffer.h"
#include "envoy/network/io_handle.h"

#include "openssl/bio.h"
#include "openssl/err.h"

namespace Envoy {
namespace Extensions {
namespace TransportSockets {
namespace Tls {

namespace {

// NOLINTNEXTLINE(readability-identifier-naming)
inline Envoy::Network::IoHandle* bio_io_handle(BIO* bio) {
return reinterpret_cast<Envoy::Network::IoHandle*>(bio->ptr);
}

// NOLINTNEXTLINE(readability-identifier-naming)
int io_handle_new(BIO* bio) {
bio->init = 0;
bio->num = -1;
bio->ptr = nullptr;
bio->flags = 0;
return 1;
}

// NOLINTNEXTLINE(readability-identifier-naming)
int io_handle_free(BIO* bio) {
if (bio == nullptr) {
return 0;
}

if (bio->shutdown) {
if (bio->init) {
bio_io_handle(bio)->close();
}
bio->init = 0;
bio->flags = 0;
}
return 1;
}

// NOLINTNEXTLINE(readability-identifier-naming)
int io_handle_read(BIO* b, char* out, int outl) {
if (out == nullptr) {
return 0;
}

Envoy::Buffer::RawSlice slice;
slice.mem_ = out;
slice.len_ = outl;
auto result = bio_io_handle(b)->readv(outl, &slice, 1);
BIO_clear_retry_flags(b);
if (!result.ok()) {
if (result.err_->getErrorCode() == Api::IoError::IoErrorCode::Again) {
BIO_set_retry_read(b);
}
return -1;
}
return result.rc_;
}

// NOLINTNEXTLINE(readability-identifier-naming)
int io_handle_write(BIO* b, const char* in, int inl) {
Envoy::Buffer::RawSlice slice;
slice.mem_ = const_cast<char*>(in);
slice.len_ = inl;
auto result = bio_io_handle(b)->writev(&slice, 1);
BIO_clear_retry_flags(b);
if (!result.ok()) {
if (result.err_->getErrorCode() == Api::IoError::IoErrorCode::Again) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may also want to set retry in the case of Api::IoError::IoErrorCode::Interrupt.

Although it seems that writes returning Interrupt will result in Envoy closing the socket. I don't know how often a EINTR return would happen in practice.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting. I guess we could treat EINTR as EAGAIN here.

BIO_set_retry_write(b);
}
return -1;
}
return result.rc_;
}

// NOLINTNEXTLINE(readability-identifier-naming)
long io_handle_ctrl(BIO* b, int cmd, long num, void* ptr) {
long ret = 1;

switch (cmd) {
case BIO_C_SET_FD:
io_handle_free(b);
b->num = -1;
b->ptr = ptr;
b->shutdown = int(num);
b->init = 1;
break;
case BIO_C_GET_FD:
ret = -1;
*reinterpret_cast<void**>(ptr) = b->ptr;
break;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need to implement BIO_C_SET_FD and BIO_C_GET_FD?

It may be good to add ASSERT(false) for cases that we do not expect to ever happen at the proxy. This includes: BIO_CTRL_RESET, BIO_C_FILE_SEEK, BIO_C_FILE_TELL, BIO_CTRL_INFO, BIO_C_GET_FD, BIO_C_SET_FD:

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've used the socket bio as an example (since we're faking one) and those are the only control messages it implements. BIO_C_SET_FD is the one I've used for setting the ptr but I guess we could change that.
What would you prefer, keep following the socket model or implement things differently?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your constructor could set b->ptr directly instead of using BIO_C_SET_FD.

I think your implementation of iohandle BIOs is a great improvement, I'm just working my way through some nits.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ow. Yes we can, but we need to initialize all the other fields (shutdown, init, num). Should I do that explicitly in the constructor and then ASSERT(false) on BIO_C_SET_FD and BIO_C_GET_FD?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we'ld need to also initiate other relevant fields like init also in the contructor.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perfect! Done!

case BIO_CTRL_GET_CLOSE:
ret = b->shutdown;
break;
case BIO_CTRL_SET_CLOSE:
b->shutdown = int(num);
break;
case BIO_CTRL_FLUSH:
ret = 1;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test for this case.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't add a test for this because it's exercised by the rest of the boringssl infra in other tests, but let me add a simple one for completion.

break;
default:
ret = 0;
break;
}
return ret;
}

const BIO_METHOD methods_io_handlep = {
BIO_TYPE_SOCKET, "io_handle",
io_handle_write, io_handle_read,
nullptr /* puts */, nullptr /* gets, */,
io_handle_ctrl, io_handle_new,
io_handle_free, nullptr /* callback_ctrl */,
};

} // namespace

// NOLINTNEXTLINE(readability-identifier-naming)
const BIO_METHOD* BIO_s_io_handle(void) { return &methods_io_handlep; }

// NOLINTNEXTLINE(readability-identifier-naming)
BIO* BIO_new_io_handle(Envoy::Network::IoHandle* io_handle) {
BIO* ret;

ret = BIO_new(BIO_s_io_handle());
RELEASE_ASSERT(ret != nullptr, "");
BIO_ctrl(ret, BIO_C_SET_FD, 0, io_handle);
return ret;
}

} // namespace Tls
} // namespace TransportSockets
} // namespace Extensions
} // namespace Envoy
21 changes: 21 additions & 0 deletions source/extensions/transport_sockets/tls/io_handle_bio.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#pragma once

#include "envoy/network/io_handle.h"

#include "openssl/bio.h"

namespace Envoy {
namespace Extensions {
namespace TransportSockets {
namespace Tls {

// NOLINTNEXTLINE(readability-identifier-naming)
const BIO_METHOD* BIO_s_io_handle(void);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method seems to only be used in the cc file, should we remove it from the header?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I exposed it for testing but ended up not using it.


// NOLINTNEXTLINE(readability-identifier-naming)
BIO* BIO_new_io_handle(Envoy::Network::IoHandle* io_handle);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible to add some comments for this factory method?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!


} // namespace Tls
} // namespace TransportSockets
} // namespace Extensions
} // namespace Envoy
6 changes: 3 additions & 3 deletions source/extensions/transport_sockets/tls/ssl_socket.cc
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include "common/common/hex.h"
#include "common/http/headers.h"

#include "extensions/transport_sockets/tls/io_handle_bio.h"
#include "extensions/transport_sockets/tls/utility.h"

#include "absl/strings/str_replace.h"
Expand Down Expand Up @@ -67,9 +68,8 @@ void SslSocket::setTransportSocketCallbacks(Network::TransportSocketCallbacks& c
provider->registerPrivateKeyMethod(rawSsl(), *this, callbacks_->connection().dispatcher());
}

// TODO(fcoras): consider using BIO_s_mem or a BIO with custom read/write functions instead of a
// socket BIO which relies on access to a file descriptor.
BIO* bio = BIO_new_socket(callbacks_->ioHandle().fdDoNotUse(), 0);
// Use custom BIO that reads from/writes to IoHandle
BIO* bio = BIO_new_io_handle(&callbacks_->ioHandle());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider protecting with a runtime feature to allow for fast rollback if we find some problem despite it being relatively simple and as far I can tell, correct.

Copy link
Member Author

@florincoras florincoras Sep 3, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, let me add that. But, since I'm not familiar with them, will switching be possible at runtime or does the code have to be recompiled with the feature moved from enabled to disabled list?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding is that it is controllable via reloadable config. The default is to use the new code path. Since this call only happens once per connection, during initialization of the transport socket object, it is safe for the runtime feature to change during runtime.

SSL_set_bio(rawSsl(), bio, bio);
}

Expand Down
11 changes: 11 additions & 0 deletions test/extensions/transport_sockets/tls/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,17 @@ envoy_cc_test(
],
)

envoy_cc_test(
name = "io_handle_bio_test",
srcs = ["io_handle_bio_test.cc"],
external_deps = ["ssl"],
deps = [
":ssl_test_utils",
"//source/extensions/transport_sockets/tls:ssl_socket_lib",
"//test/mocks/network:io_handle_mocks",
],
)

envoy_cc_test(
name = "utility_test",
srcs = [
Expand Down
62 changes: 62 additions & 0 deletions test/extensions/transport_sockets/tls/io_handle_bio_test.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#include "common/network/io_socket_error_impl.h"

#include "extensions/transport_sockets/tls/io_handle_bio.h"

#include "test/mocks/network/io_handle.h"

#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "openssl/ssl.h"

using testing::NiceMock;
using testing::Return;

namespace Envoy {
namespace Extensions {
namespace TransportSockets {
namespace Tls {

class IoHandleBioTest : public testing::Test {
public:
IoHandleBioTest() { bio_ = BIO_new_io_handle(&io_handle_); }
~IoHandleBioTest() override { BIO_free(bio_); }

BIO* bio_;
NiceMock<Network::MockIoHandle> io_handle_;
};

TEST_F(IoHandleBioTest, TestMiscApis) {
EXPECT_EQ(bio_->method->destroy(nullptr), 0);
EXPECT_EQ(bio_->method->bread(nullptr, nullptr, 0), 0);

int ret;
NiceMock<Network::MockIoHandle>* ptr;

ret = bio_->method->ctrl(bio_, BIO_C_GET_FD, 0, &ptr);
EXPECT_EQ(ret, -1);
EXPECT_EQ(ptr, &io_handle_);

ret = bio_->method->ctrl(bio_, BIO_CTRL_RESET, 0, nullptr);
EXPECT_EQ(ret, 0);

ret = bio_->method->ctrl(bio_, BIO_CTRL_SET_CLOSE, 1, nullptr);
EXPECT_EQ(ret, 1);

ret = bio_->method->ctrl(bio_, BIO_CTRL_GET_CLOSE, 0, nullptr);
EXPECT_EQ(ret, 1);

// avoid BIO_free assert in destructor
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bio_->init = 1 and bio_->shutdown = 1 further down seem to undo the work done by this BIO_CTRL_SET_CLOSE call.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed this test. It was crashing only prior to adding the last one.

ret = bio_->method->ctrl(bio_, BIO_CTRL_SET_CLOSE, 0, nullptr);
EXPECT_EQ(ret, 1);

EXPECT_CALL(io_handle_, close())
.WillOnce(Return(testing::ByMove(Api::IoCallUint64Result{
0, Api::IoErrorPtr(nullptr, Network::IoSocketError::deleteIoError)})));
bio_->init = 1;
bio_->shutdown = 1;
}

} // namespace Tls
} // namespace TransportSockets
} // namespace Extensions
} // namespace Envoy