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

bidirectional gRPC outputs + fix server streaming gRPC outputs #1241

Merged
merged 19 commits into from
Jun 29, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
fa9ba19
fix(userspace/falco): virtual destructor of base grpc context
leodido May 26, 2020
b4022fb
update(userspace/falco): unsafe_size() method for falco::output::queue
leodido May 27, 2020
15ef0cb
update(userspace/falco): context class for bidirectional gRPC services
leodido May 27, 2020
e09509c
new(userspace/falco): output gRPC service to provide a server streami…
leodido May 27, 2020
47f830c
new(userspace/falco): macro to REGISTER_BIDI gRPC services
leodido May 27, 2020
2e562fa
new(userspace/falco): gRPC context for bidirectional services
leodido May 27, 2020
8e2a50e
new(userspace/falco): concrete initial implementation of the subscrib…
leodido May 27, 2020
fa41be7
wip(userspace/falco): bidirectional gRPC outputs logic (initial)
leodido May 28, 2020
93eb326
update(userspace/falco/grpc): dealing with multiple streaming requests
fntlnz May 28, 2020
3e774f6
update(userspace/falco/grpc): for stream contexts use a flag to detect
fntlnz May 29, 2020
c345af3
update(userspace/falco/grpc): bidirectional sub implementation
fntlnz May 29, 2020
2c9c583
update(userspace/falco/grpc): simpler bidirectional context state
fntlnz May 29, 2020
8764bba
update(userspace/falco): remove output queue size
fntlnz May 29, 2020
47e71a2
update(userspace/falco): remove keepalive from output request
fntlnz May 29, 2020
1f6dcaa
update(proposals): keep Falco gRPC Outputs proposal in sync
leodido May 29, 2020
f5688f1
update(userspace/falco): pluralize Falco output proto and service
leodido May 29, 2020
759aaa4
bc(userspace/falco): the Falco gRPC Outputs API are now "falco.output…
leodido May 29, 2020
b7155bf
update(userspace/falco): better gRPC server logging
fntlnz Jun 1, 2020
cd858c0
update(userspace/falco): avoid memory allocation for falco output
fntlnz Jun 5, 2020
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
32 changes: 17 additions & 15 deletions proposals/20190826-grpc-outputs.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# gRPC Falco Output
# Falco gRPC Outputs

<!-- toc -->

Expand All @@ -25,7 +25,7 @@ An alert is an "output" when it goes over a transport, and it is emitted by Falc

At the current moment, however, Falco can deliver alerts in a very basic way, for example by dumping them to standard output.

For this reason, many Falco users asked, with issues - eg., [falco#528](https://github.com/falcosecurity/falco/issues/528) - or in the [slack channel](https://sysdig.slack.com) if we can find a more consumable way to implement Falco outputs in an extensible way.
For this reason, many Falco users asked, with issues - eg., [falco#528](https://github.com/falcosecurity/falco/issues/528) - or in the [slack channel](https://slack.k8s.io) if we can find a more consumable way to implement Falco outputs in an extensible way.

The motivation behind this proposal is to design a new output implementation that can meet our user's needs.

Expand All @@ -39,7 +39,10 @@ The motivation behind this proposal is to design a new output implementation tha
- To continue supporting the old output formats by implementing their same interface
- To be secure by default (**mutual TLS** authentication)
- To be **asynchronous** and **non-blocking**
- To implement a Go SDK
- To provide a connection over unix socket (no authentication)
- To implement a Go client
- To implement a Rust client
- To implement a Python client

### Non-Goals

Expand Down Expand Up @@ -77,26 +80,25 @@ syntax = "proto3";
import "google/protobuf/timestamp.proto";
import "schema.proto";

package falco.output;
package falco.outputs;

option go_package = "github.com/falcosecurity/client-go/pkg/api/output";
option go_package = "github.com/falcosecurity/client-go/pkg/api/outputs";

// The `subscribe` service defines the RPC call
// to perform an output `request` which will lead to obtain an output `response`.
// This service defines the RPC methods
// to `request` a stream of output `response`s.
service service {
rpc subscribe(request) returns (stream response);
// Subscribe to a stream of Falco outputs by sending a stream of requests.
rpc sub(stream request) returns (stream response);
// Get all the Falco outputs present in the system up to this call.
rpc get(request) returns (stream response);
}

// The `request` message is the logical representation of the request model.
// It is the input of the `subscribe` service.
// It is used to configure the kind of subscription to the gRPC streaming server.
// It is the input of the `output.service` service.
message request {
bool keepalive = 1;
// string duration = 2; // TODO(leodido, fntlnz): not handled yet but keeping for reference.
// repeated string tags = 3; // TODO(leodido, fntlnz): not handled yet but keeping for reference.
}

// The `response` message is the logical representation of the output model.
// The `response` message is the representation of the output model.
// It contains all the elements that Falco emits in an output along with the
// definitions for priorities and source.
message response {
Expand All @@ -106,7 +108,7 @@ message response {
string rule = 4;
string output = 5;
map<string, string> output_fields = 6;
// repeated string tags = 7; // TODO(leodido,fntlnz): tags not supported yet, keeping for reference
string hostname = 7;
}
```

Expand Down
23 changes: 12 additions & 11 deletions userspace/falco/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,24 @@ add_custom_command(
${CMAKE_CURRENT_BINARY_DIR}/version.grpc.pb.h
${CMAKE_CURRENT_BINARY_DIR}/version.pb.cc
${CMAKE_CURRENT_BINARY_DIR}/version.pb.h
${CMAKE_CURRENT_BINARY_DIR}/output.grpc.pb.cc
${CMAKE_CURRENT_BINARY_DIR}/output.grpc.pb.h
${CMAKE_CURRENT_BINARY_DIR}/output.pb.cc
${CMAKE_CURRENT_BINARY_DIR}/output.pb.h
${CMAKE_CURRENT_BINARY_DIR}/outputs.grpc.pb.cc
${CMAKE_CURRENT_BINARY_DIR}/outputs.grpc.pb.h
${CMAKE_CURRENT_BINARY_DIR}/outputs.pb.cc
${CMAKE_CURRENT_BINARY_DIR}/outputs.pb.h
${CMAKE_CURRENT_BINARY_DIR}/schema.pb.cc
${CMAKE_CURRENT_BINARY_DIR}/schema.pb.h
COMMENT "Generate gRPC version API"
COMMENT "Generate gRPC API"
# Falco gRPC Version API
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/version.proto
COMMAND ${PROTOC} -I ${CMAKE_CURRENT_SOURCE_DIR} --cpp_out=. ${CMAKE_CURRENT_SOURCE_DIR}/version.proto
COMMAND ${PROTOC} -I ${CMAKE_CURRENT_SOURCE_DIR} --grpc_out=. --plugin=protoc-gen-grpc=${GRPC_CPP_PLUGIN}
${CMAKE_CURRENT_SOURCE_DIR}/version.proto
COMMENT "Generate gRPC outputs API"
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/output.proto
COMMAND ${PROTOC} -I ${CMAKE_CURRENT_SOURCE_DIR} --cpp_out=. ${CMAKE_CURRENT_SOURCE_DIR}/output.proto
# Falco gRPC Outputs API
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/outputs.proto
COMMAND ${PROTOC} -I ${CMAKE_CURRENT_SOURCE_DIR} --cpp_out=. ${CMAKE_CURRENT_SOURCE_DIR}/outputs.proto
${CMAKE_CURRENT_SOURCE_DIR}/schema.proto
COMMAND ${PROTOC} -I ${CMAKE_CURRENT_SOURCE_DIR} --grpc_out=. --plugin=protoc-gen-grpc=${GRPC_CPP_PLUGIN}
${CMAKE_CURRENT_SOURCE_DIR}/output.proto
${CMAKE_CURRENT_SOURCE_DIR}/outputs.proto
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR})

add_executable(
Expand All @@ -54,8 +55,8 @@ add_executable(
grpc_server.cpp
${CMAKE_CURRENT_BINARY_DIR}/version.grpc.pb.cc
${CMAKE_CURRENT_BINARY_DIR}/version.pb.cc
${CMAKE_CURRENT_BINARY_DIR}/output.grpc.pb.cc
${CMAKE_CURRENT_BINARY_DIR}/output.pb.cc
${CMAKE_CURRENT_BINARY_DIR}/outputs.grpc.pb.cc
${CMAKE_CURRENT_BINARY_DIR}/outputs.pb.cc
${CMAKE_CURRENT_BINARY_DIR}/schema.pb.cc)

add_dependencies(falco civetweb string-view-lite)
Expand Down
7 changes: 3 additions & 4 deletions userspace/falco/falco_outputs.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,10 @@ limitations under the License.

#include "formats.h"
#include "logger.h"
#include "falco_output_queue.h"
#include "falco_outputs_queue.h"
Copy link
Contributor

Choose a reason for hiding this comment

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

Knowing you @leodido you put a lot of thought into this.

Can you share your thinking behind the change. As I start digging more and more into the code I will be opening up more PRs and would like to understand your thinking before suggesting changes.

#include "banned.h" // This raises a compilation error when certain functions are used

using namespace std;
using namespace falco::output;

const static struct luaL_reg ll_falco_outputs [] =
{
Expand Down Expand Up @@ -316,7 +315,7 @@ int falco_outputs::handle_grpc(lua_State *ls)
lua_error(ls);
}

response grpc_res = response();
falco::outputs::response grpc_res;

// time
gen_event *evt = (gen_event *)lua_topointer(ls, 1);
Expand Down Expand Up @@ -366,7 +365,7 @@ int falco_outputs::handle_grpc(lua_State *ls)
auto host = grpc_res.mutable_hostname();
*host = (char *)lua_tostring(ls, 7);

falco::output::queue::get().push(grpc_res);
falco::outputs::queue::get().push(grpc_res);

return 1;
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ limitations under the License.

#pragma once

#include "output.pb.h"
#include "outputs.pb.h"
#include "tbb/concurrent_queue.h"

namespace falco
{
namespace output
namespace outputs
{
typedef tbb::concurrent_queue<response> response_cq;

Copy link
Contributor

Choose a reason for hiding this comment

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

It looks like queue has some public methods/members. I know this is a nit, and is out of scope for this PR but some doc blocks that reference how we are supposed to use these methods would be useful.

The output queue is going to be very important as Falco gains adoptions so spending a few minutes to write down how others should use them might make sense.

Traditionally I have also made it a point to at the minimum do this in the public members of the header files, but however we decide to do it would be great!

Feel free to ignore this if we want to bring up a broader coding style/documentation discussion as a community. Just sharing thoughts as I look at the code.

Expand Down
13 changes: 11 additions & 2 deletions userspace/falco/grpc_context.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class context
{
public:
context(::grpc::ServerContext* ctx);
~context() = default;
virtual ~context() = default;
Copy link
Contributor

Choose a reason for hiding this comment

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

Same here with context, why virtual now? and what does context mean to you?

Copy link
Member Author

Choose a reason for hiding this comment

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

The rationale is: context is a base class and instances of the derived classes get destructed.

When a base class destructor is not virtual and you have a variable pointing to an object derived from the base class, deleting the derived instance has undefined behaviour. Which can lead to memory leaks.


void get_metadata(std::string key, std::string& val);

Expand All @@ -50,7 +50,7 @@ class stream_context : public context
public:
stream_context(::grpc::ServerContext* ctx):
context(ctx){};
~stream_context() = default;
virtual ~stream_context() = default;

enum : char
{
Expand All @@ -61,6 +61,15 @@ class stream_context : public context

mutable void* m_stream = nullptr; // todo(fntlnz, leodido) > useful in the future
mutable bool m_has_more = false;
mutable bool m_is_running = true;
};

class bidi_context : public stream_context
{
public:
bidi_context(::grpc::ServerContext* ctx):
stream_context(ctx){};
virtual ~bidi_context() = default;
};

} // namespace grpc
Expand Down
124 changes: 101 additions & 23 deletions userspace/falco/grpc_request_context.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@ namespace grpc
{

template<>
void request_stream_context<falco::output::service, falco::output::request, falco::output::response>::start(server* srv)
void request_stream_context<outputs::service, outputs::request, outputs::response>::start(server* srv)
{
m_state = request_context_base::REQUEST;
m_srv_ctx.reset(new ::grpc::ServerContext);
auto srvctx = m_srv_ctx.get();
m_res_writer.reset(new ::grpc::ServerAsyncWriter<output::response>(srvctx));
m_res_writer.reset(new ::grpc::ServerAsyncWriter<outputs::response>(srvctx));
m_stream_ctx.reset();
m_req.Clear();
auto cq = srv->m_completion_queue.get();
Expand All @@ -38,7 +38,7 @@ void request_stream_context<falco::output::service, falco::output::request, falc
}

template<>
void request_stream_context<falco::output::service, falco::output::request, falco::output::response>::process(server* srv)
void request_stream_context<outputs::service, outputs::request, outputs::response>::process(server* srv)
{
// When it is the 1st process call
if(m_state == request_context_base::REQUEST)
Expand All @@ -48,40 +48,46 @@ void request_stream_context<falco::output::service, falco::output::request, falc
}

// Processing
output::response res;
(srv->*m_process_func)(*m_stream_ctx, m_req, res); // subscribe()
outputs::response res;
(srv->*m_process_func)(*m_stream_ctx, m_req, res); // get()

if(!m_stream_ctx->m_is_running)
{
m_state = request_context_base::FINISH;
m_res_writer->Finish(::grpc::Status::OK, this);
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is Finish() capitalized? I haven't seen many other capital methods in the code.

Copy link
Member Author

Choose a reason for hiding this comment

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

return;
}

// When there are still more responses to stream
if(m_stream_ctx->m_has_more)
{
// todo(leodido) > log "write: tag=this, state=m_state"
m_res_writer->Write(res, this);
return;
}

// No more responses to stream
else
{
// Communicate to the gRPC runtime that we have finished.
// The memory address of "this" instance uniquely identifies the event.
m_state = request_context_base::FINISH;
// todo(leodido) > log "finish: tag=this, state=m_state"
m_res_writer->Finish(::grpc::Status::OK, this);
}
// Communicate to the gRPC runtime that we have finished.
// The memory address of "this" instance uniquely identifies the event.
m_state = request_context_base::FINISH;
// todo(leodido) > log "finish: tag=this, state=m_state"
m_res_writer->Finish(::grpc::Status::OK, this);
}

template<>
void request_stream_context<falco::output::service, falco::output::request, falco::output::response>::end(server* srv, bool errored)
void request_stream_context<outputs::service, outputs::request, outputs::response>::end(server* srv, bool error)
{
if(m_stream_ctx)
{
if(errored)
if(error)
{
// todo(leodido) > log error "error streaming: tag=this, state=m_state, stream=m_stream_ctx->m_stream"
}
m_stream_ctx->m_status = errored ? stream_context::ERROR : stream_context::SUCCESS;
m_stream_ctx->m_status = error ? stream_context::ERROR : stream_context::SUCCESS;

// Complete the processing
output::response res;
(srv->*m_process_func)(*m_stream_ctx, m_req, res); // subscribe()
outputs::response res;
(srv->*m_process_func)(*m_stream_ctx, m_req, res); // get()
}
else
{
Expand All @@ -98,7 +104,7 @@ void request_stream_context<falco::output::service, falco::output::request, falc
}

template<>
void falco::grpc::request_context<falco::version::service, falco::version::request, falco::version::response>::start(server* srv)
void request_context<version::service, version::request, version::response>::start(server* srv)
{
m_state = request_context_base::REQUEST;
m_srv_ctx.reset(new ::grpc::ServerContext);
Expand All @@ -113,7 +119,7 @@ void falco::grpc::request_context<falco::version::service, falco::version::reque
}

template<>
void falco::grpc::request_context<falco::version::service, falco::version::request, falco::version::response>::process(server* srv)
void request_context<version::service, version::request, version::response>::process(server* srv)
{
version::response res;
(srv->*m_process_func)(m_srv_ctx.get(), m_req, res);
Expand All @@ -125,13 +131,85 @@ void falco::grpc::request_context<falco::version::service, falco::version::reque
}

template<>
void falco::grpc::request_context<falco::version::service, falco::version::request, falco::version::response>::end(server* srv, bool errored)
void request_context<version::service, version::request, version::response>::end(server* srv, bool error)
{
// todo(leodido) > handle processing errors here

// Ask to start processing requests
start(srv);
}

template<>
void request_bidi_context<outputs::service, outputs::request, outputs::response>::start(server* srv)
{
m_state = request_context_base::REQUEST;
m_srv_ctx.reset(new ::grpc::ServerContext);
auto srvctx = m_srv_ctx.get();
m_reader_writer.reset(new ::grpc::ServerAsyncReaderWriter<outputs::response, outputs::request>(srvctx));
m_req.Clear();
auto cq = srv->m_completion_queue.get();
// Request to start processing given requests.
// Using "this" - ie., the memory address of this context - as the tag that uniquely identifies the request.
// In this way, different contexts can serve different requests concurrently.
(srv->m_output_svc.*m_request_func)(srvctx, m_reader_writer.get(), cq, cq, this);
};

template<>
void request_bidi_context<outputs::service, outputs::request, outputs::response>::process(server* srv)
{
switch(m_state)
{
case request_context_base::REQUEST:
m_bidi_ctx.reset(new bidi_context(m_srv_ctx.get()));
m_bidi_ctx->m_status = bidi_context::STREAMING;
m_state = request_context_base::WRITE;
m_reader_writer->Read(&m_req, this);
return;
case request_context_base::WRITE:
// Processing
{
outputs::response res;
(srv->*m_process_func)(*m_bidi_ctx, m_req, res); // sub()

if(!m_bidi_ctx->m_is_running)
{
m_state = request_context_base::FINISH;
m_reader_writer->Finish(::grpc::Status::OK, this);
return;
}

if(m_bidi_ctx->m_has_more)
{
m_state = request_context_base::WRITE;
m_reader_writer->Write(res, this);
return;
}

m_state = request_context_base::WRITE;
m_reader_writer->Read(&m_req, this);
}

return;
default:
return;
}
};

template<>
void request_bidi_context<outputs::service, outputs::request, outputs::response>::end(server* srv, bool error)
{
if(m_bidi_ctx)
{
m_bidi_ctx->m_status = error ? bidi_context::ERROR : bidi_context::SUCCESS;

// Complete the processing
outputs::response res;
(srv->*m_process_func)(*m_bidi_ctx, m_req, res); // sub()
}

// Ask to start processing requests
start(srv);
};

} // namespace grpc
} // namespace falco
} // namespace falco
Loading