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

contrib/envoyproxy: envoy external processing support #2895

Open
wants to merge 12 commits into
base: main
Choose a base branch
from

Conversation

e-n-0
Copy link
Member

@e-n-0 e-n-0 commented Sep 27, 2024

Motivation

This is the part 1 PR to support Envoy's External Processing.
You can find all related document for this implementation in Confluence ASM - GCP Services Extensions.
You can find the part 2 of this PR here.

What does this PR do?

This PR adds a new gRPC Interceptor (StreamServerInterceptor) to support the interception of ext_proc v3 calls to gRPC server. When the interceptor is applied, all messages of the external processing protocol are instrumented without returning an handle to the original server code. The implementation of a server using this instrumentation can be found in the part 2.

The implementation includes:

  • Analysing synchronously HTTP Requests and Responses data (headers, ip, path, host, status code)
  • Blocking requests (supporting blocking with content-type and redirect)
  • Some refacto of existing code to handle more easily crafted requests without an actual valid http.Request object.

Tests

This PR includes unit testing in the envoy_tests.go, simulating scenarios of malicious or benign requests, validating span tags, security events and blocking results.

System-tests have been implemented on this PR. A new external-processing scenario has been added in the golang stage.

Reviewer's Checklist

  • Changed code has unit tests for its functionality at or near 100% coverage.
  • System-Tests covering this feature have been added and enabled with the va.b.c-dev version tag.
  • There is a benchmark for any new code, or changes to existing code.
  • If this interacts with the agent in a new way, a system test has been added.
  • Add an appropriate team label so this PR gets put in the right place for the release notes.
  • Non-trivial go.mod changes, e.g. adding new modules, are reviewed by @DataDog/dd-trace-go-guild.

Unsure? Have a question? Request a review!

@DataDog DataDog deleted a comment from datadog-datadog-prod-us1 bot Sep 27, 2024
@DataDog DataDog deleted a comment from datadog-datadog-prod-us1 bot Sep 27, 2024
@DataDog DataDog deleted a comment from datadog-datadog-prod-us1 bot Sep 27, 2024
@DataDog DataDog deleted a comment from datadog-datadog-prod-us1 bot Sep 27, 2024
@DataDog DataDog deleted a comment from datadog-datadog-prod-us1 bot Sep 27, 2024
@DataDog DataDog deleted a comment from datadog-datadog-prod-us1 bot Sep 27, 2024
@e-n-0 e-n-0 force-pushed the flavien/service-extensions branch 7 times, most recently from fcbd354 to a587a09 Compare September 27, 2024 13:33
@pr-commenter
Copy link

pr-commenter bot commented Sep 27, 2024

Benchmarks

Benchmark execution time: 2024-12-11 11:03:14

Comparing candidate commit ee0cd57 in PR branch flavien/service-extensions with baseline commit a48a41e in branch main.

Found 0 performance improvements and 0 performance regressions! Performance is the same for 59 metrics, 0 unstable metrics.

@e-n-0 e-n-0 force-pushed the flavien/service-extensions branch 16 times, most recently from 4134743 to a96cc2a Compare September 30, 2024 15:26
@e-n-0 e-n-0 requested review from a team as code owners November 6, 2024 16:50
Copy link
Contributor

@eliottness eliottness left a comment

Choose a reason for hiding this comment

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

Thanks for contributing 🙏. Couple of requests:

  • Normally you should not have to import any listener packages, nor to export functions from it.
  • Could you link to a manual CI run from the System Tests workflow pointing on your system-tests branch where the workflow run is green ? You probably have to add your scenario in the workflow for it to run
  • I suspect we need to add tests for the changes you did in the emitter/waf/actions package. Let's see if this would make sense to separate it in another PR
  • Let's review together the code of envoy.go on fridayso we can structure it better

internal/appsec/emitter/httpsec/http.go Outdated Show resolved Hide resolved
internal/appsec/emitter/httpsec/http.go Outdated Show resolved Hide resolved
internal/appsec/emitter/httpsec/http.go Outdated Show resolved Hide resolved
internal/appsec/emitter/waf/actions/http_redirect.go Outdated Show resolved Hide resolved
internal/appsec/listener/httpsec/request.go Outdated Show resolved Hide resolved
contrib/envoyproxy/envoy/envoy.go Outdated Show resolved Hide resolved
contrib/envoyproxy/envoy/envoy.go Outdated Show resolved Hide resolved
contrib/envoyproxy/envoy/envoy.go Outdated Show resolved Hide resolved
contrib/envoyproxy/envoy/envoy.go Outdated Show resolved Hide resolved
contrib/envoyproxy/envoy/envoy.go Outdated Show resolved Hide resolved
@e-n-0 e-n-0 force-pushed the flavien/service-extensions branch from bdd1915 to 1f2b791 Compare November 8, 2024 14:25
@eliottness eliottness force-pushed the flavien/service-extensions branch 2 times, most recently from 9630788 to 0929de5 Compare November 12, 2024 12:54
@eliottness
Copy link
Contributor

Consolidated system-test run: https://github.com/DataDog/dd-trace-go/actions/runs/11797728094

Comment on lines +19 to +25
// ResetStatusCode resets the status code of the response writer.
func ResetStatusCode(w http.ResponseWriter) {
if rw, ok := w.(*responseWriter); ok {
rw.status = 0
}
}

Copy link
Contributor

Choose a reason for hiding this comment

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

This required because of this if statement.

@eliottness eliottness changed the title appsec: envoy external processing support contrib/envoyproxy: envoy external processing support Nov 12, 2024
@eliottness eliottness force-pushed the flavien/service-extensions branch from 0929de5 to e3bb1e5 Compare November 13, 2024 16:14
contrib/envoyproxy/envoy/envoy.go Outdated Show resolved Hide resolved
contrib/envoyproxy/envoy/envoy.go Outdated Show resolved Hide resolved
contrib/envoyproxy/envoy/envoy.go Outdated Show resolved Hide resolved
contrib/envoyproxy/envoy/fakehttp.go Outdated Show resolved Hide resolved
contrib/envoyproxy/envoy/fakehttp.go Outdated Show resolved Hide resolved
contrib/envoyproxy/envoy/fakehttp.go Outdated Show resolved Hide resolved
contrib/envoyproxy/envoy/fakehttp.go Outdated Show resolved Hide resolved
contrib/envoyproxy/envoy/envoy.go Outdated Show resolved Hide resolved
contrib/envoyproxy/envoy/envoy_test.go Outdated Show resolved Hide resolved
contrib/envoyproxy/envoy/envoy.go Outdated Show resolved Hide resolved
@e-n-0 e-n-0 force-pushed the flavien/service-extensions branch from c7063d8 to b0844b8 Compare November 28, 2024 14:59
@e-n-0 e-n-0 requested a review from rarguelloF December 11, 2024 10:09
@e-n-0 e-n-0 force-pushed the flavien/service-extensions branch from b0844b8 to 961d73d Compare December 11, 2024 10:14
@datadog-datadog-prod-us1
Copy link

Datadog Report

Branch report: flavien/service-extensions
Commit report: d008bfb
Test service: dd-trace-go

✅ 0 Failed, 5110 Passed, 70 Skipped, 2m 52.72s Total Time

wrappedResponseWriter http.ResponseWriter
}

func StreamServerInterceptor(opts ...grpctrace.Option) grpc.StreamServerInterceptor {
Copy link
Contributor

Choose a reason for hiding this comment

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

Please add a godoc to the exported stuff 🙏 Also it would be great to briefly explain what this actually does, as it took me some time to figure out.

Suggested change
func StreamServerInterceptor(opts ...grpctrace.Option) grpc.StreamServerInterceptor {
// StreamServerInterceptor returns a new grpc.StreamServerInterceptor intended to be used with an envoyproxy grpc server.
// It will use the regular grpc tracing package for all methods except from `envoy.service.ext_proc.v3.ExternalProcessor/Process` where it will [...]
func StreamServerInterceptor(opts ...grpctrace.Option) grpc.StreamServerInterceptor {

(please feel free to modify the comment if I wrote anything incorrect, and add more context as you did in the PR description).

Comment on lines +16 to +29
// Create a listener for the server.
ln, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatal(err)
}

// Create the server interceptor using the envoy go control plane package.
si := go_control_plane.StreamServerInterceptor()

// Initialize the grpc server as normal, using the envoy server interceptor.
s := grpc.NewServer(grpc.StreamInterceptor(si))

// ... register your services

Copy link
Contributor

Choose a reason for hiding this comment

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

Could you add a more "real-world" like example? similar to https://github.com/envoyproxy/go-control-plane/blob/main/examples/dyplomat/main.go#L43-L53

(currently this example is just a generic grpc server without any envoyproxy stuff)

}
}

func ProcessRequestHeaders(ctx context.Context, req *envoyextproc.ProcessingRequest_RequestHeaders) (*envoyextproc.ProcessingResponse, *currentRequest, bool, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

please add a godoc comment, or consider unexporting this function if this is not intended to be used directly.

}
}()

for {
Copy link
Contributor

Choose a reason for hiding this comment

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

I find this pattern a little bit odd for an interceptor / middleware.

Since it seems this is pretty much specifically intended to override the behaviour of ext_procv3.ExternalProcessorServer.Process, have you considered exporting this functionality as an implementation of this interface instead of a middleware? This way, users could just do:

import envoytrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/envoyproxy/go-control-plane"

// srv would be the user provided implementation of `ext_procv3.ExternalProcessorServer`
appsecBlockSrv := envoytrace.AppsecBlockingProcessorServer(srv) // internally you would call srv.Process() when the request is not blocked
ext_procv3.RegisterExternalProcessorServer(grpcServer, appsecBlockSrv)

if err != nil {
// Note: Envoy is inconsistent with the "end_of_stream" value of its headers responses,
// so we can't fully rely on it to determine when it will close (cancel) the stream.
if err == io.EOF || err.(interface{ GRPCStatus() *status.Status }).GRPCStatus().Code() == codes.Canceled {
Copy link
Contributor

Choose a reason for hiding this comment

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

you can use s, ok := status.FromError(err) to do the assertion safely.

}, nil

case *envoyextproc.ProcessingRequest_ResponseBody:
r := req.Request.(*envoyextproc.ProcessingRequest_ResponseBody)
Copy link
Contributor

Choose a reason for hiding this comment

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

this line is not necessary, the type asserted variable is already available in v

Suggested change
r := req.Request.(*envoyextproc.ProcessingRequest_ResponseBody)
r := v

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
apm:ecosystem contrib/* related feature requests or bugs appsec
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants