From 4327d106825268aed996046d144b754d14adbdc6 Mon Sep 17 00:00:00 2001 From: Flavien Darche Date: Fri, 30 Aug 2024 16:54:57 +0200 Subject: [PATCH 01/14] Service Extension Callout (Envoy external processing) --- .../workflows/service-extensions-publish.yml | 99 +++ .../envoy/cmd/serviceextensions/.gitignore | 1 + .../envoy/cmd/serviceextensions/Dockerfile | 20 + .../envoy/cmd/serviceextensions/localhost.crt | 19 + .../envoy/cmd/serviceextensions/localhost.key | 27 + .../envoy/cmd/serviceextensions/main.go | 128 ++++ contrib/envoyproxy/envoy/envoy.go | 420 +++++++++++++ contrib/envoyproxy/envoy/envoy_test.go | 569 ++++++++++++++++++ contrib/internal/httptrace/httptrace.go | 78 ++- go.mod | 14 +- go.sum | 13 + internal/appsec/emitter/httpsec/http.go | 47 ++ internal/appsec/emitter/waf/actions/block.go | 60 +- .../emitter/waf/actions/http_redirect.go | 88 ++- internal/appsec/listener/httpsec/http.go | 4 +- internal/appsec/listener/httpsec/request.go | 8 +- .../appsec/listener/httpsec/request_test.go | 4 +- internal/appsec/listener/trace/trace.go | 7 + internal/appsec/testdata/user_rules.json | 27 + 19 files changed, 1591 insertions(+), 42 deletions(-) create mode 100644 .github/workflows/service-extensions-publish.yml create mode 100644 contrib/envoyproxy/envoy/cmd/serviceextensions/.gitignore create mode 100644 contrib/envoyproxy/envoy/cmd/serviceextensions/Dockerfile create mode 100644 contrib/envoyproxy/envoy/cmd/serviceextensions/localhost.crt create mode 100644 contrib/envoyproxy/envoy/cmd/serviceextensions/localhost.key create mode 100644 contrib/envoyproxy/envoy/cmd/serviceextensions/main.go create mode 100644 contrib/envoyproxy/envoy/envoy.go create mode 100644 contrib/envoyproxy/envoy/envoy_test.go diff --git a/.github/workflows/service-extensions-publish.yml b/.github/workflows/service-extensions-publish.yml new file mode 100644 index 0000000000..9e0c645be0 --- /dev/null +++ b/.github/workflows/service-extensions-publish.yml @@ -0,0 +1,99 @@ +name: Publish Service Extensions Callout images packages + +on: + push: + branches: + - 'flavien/service-extensions' + release: + types: + - published + workflow_dispatch: + inputs: + tag_name: + description: 'Docker image tag to use for the package' + required: true + default: 'dev' + commit_sha: + description: 'Commit SHA to checkout' + required: true + +permissions: + contents: read + packages: write + +jobs: + publish-service-extensions: + runs-on: ubuntu-latest + steps: + + - name: Get tag name + id: get_tag_name + run: | + if [ "${{ github.event_name }}" = "release" ]; then + echo "::set-output name=tag::${{ github.event.release.tag_name }}" + echo "Here1: tag=${{ github.event.release.tag_name }}" + else + if [ -z "${{ github.event.inputs.tag_name }}" ]; then + echo "::set-output name=tag::dev" + echo "Here2: tag=dev" + else + echo "::set-output name=tag::${{ github.event.inputs.tag_name }}" + echo "Here3: tag=${{ github.event.inputs.tag_name }}" + fi + fi + echo "Finally: ${{ steps.get_tag_name.outputs.tag }}" + + - name: Checkout + uses: actions/checkout@v4 + if: github.event_name == 'release' + with: + ref: ${{ steps.get_tag_name.outputs.tag }} + + - name: Checkout + uses: actions/checkout@v4 + if: github.event_name != 'release' + with: + ref: ${{ github.event.inputs.commit_sha || github.sha }} + + - name: Set up Go 1.22 + uses: actions/setup-go@v5 + with: + go-version: 1.22 + id: go + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker + shell: bash + run: docker login -u publisher -p ${{ secrets.GITHUB_TOKEN }} ghcr.io + + - name: Build and push [dev] + id: build-dev + if: github.event_name != 'release' + uses: docker/build-push-action@v6 + with: + context: . + file: ./contrib/envoyproxy/envoy/cmd/serviceextensions/Dockerfile + platforms: linux/amd64,linux/arm64 + push: true + tags: | # Use the commit SHA from the manual trigger or default to the SHA from the push event + ghcr.io/datadog/dd-trace-go/service-extensions-callout:${{ steps.get_tag_name.outputs.tag }} + ghcr.io/datadog/dd-trace-go/service-extensions-callout:${{ github.event.inputs.commit_sha || github.sha }} + + - name: Build and push [release] + id: build-release + if: github.event_name == 'release' + uses: docker/build-push-action@v6 + with: + context: . + file: ./contrib/envoyproxy/envoy/cmd/serviceextensions/Dockerfile + platforms: linux/amd64,linux/arm64 + push: true + tags: | + ghcr.io/datadog/dd-trace-go/service-extensions-callout:latest + ghcr.io/datadog/dd-trace-go/service-extensions-callout:${{ steps.get_tag_name.outputs.tag }} + ghcr.io/datadog/dd-trace-go/service-extensions-callout:${{ github.sha }} \ No newline at end of file diff --git a/contrib/envoyproxy/envoy/cmd/serviceextensions/.gitignore b/contrib/envoyproxy/envoy/cmd/serviceextensions/.gitignore new file mode 100644 index 0000000000..68295c4a55 --- /dev/null +++ b/contrib/envoyproxy/envoy/cmd/serviceextensions/.gitignore @@ -0,0 +1 @@ +serviceextensions \ No newline at end of file diff --git a/contrib/envoyproxy/envoy/cmd/serviceextensions/Dockerfile b/contrib/envoyproxy/envoy/cmd/serviceextensions/Dockerfile new file mode 100644 index 0000000000..87136d2cba --- /dev/null +++ b/contrib/envoyproxy/envoy/cmd/serviceextensions/Dockerfile @@ -0,0 +1,20 @@ +# Build stage +FROM golang:1.22-alpine AS builder +ENV CGO_ENABLED=1 +WORKDIR /app +COPY . . +RUN apk add --no-cache --update git build-base +RUN go build -o ./contrib/envoyproxy/envoy/cmd/serviceextensions/serviceextensions ./contrib/envoyproxy/envoy/cmd/serviceextensions + +# Runtime stage +FROM alpine:3.20.3 +RUN apk --no-cache add ca-certificates tzdata libc6-compat libgcc libstdc++ +WORKDIR /app +COPY --from=builder /app/contrib/envoyproxy/envoy/cmd/serviceextensions/serviceextensions /app/serviceextensions +COPY ./contrib/envoyproxy/envoy/cmd/serviceextensions/localhost.crt /app/localhost.crt +COPY ./contrib/envoyproxy/envoy/cmd/serviceextensions/localhost.key /app/localhost.key + +EXPOSE 80 +EXPOSE 443 + +CMD ["./serviceextensions"] diff --git a/contrib/envoyproxy/envoy/cmd/serviceextensions/localhost.crt b/contrib/envoyproxy/envoy/cmd/serviceextensions/localhost.crt new file mode 100644 index 0000000000..fc54fd492e --- /dev/null +++ b/contrib/envoyproxy/envoy/cmd/serviceextensions/localhost.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDFjCCAf4CCQCzrLIhrWa55zANBgkqhkiG9w0BAQsFADBCMQswCQYDVQQGEwJV +UzETMBEGA1UECAwKQ2FsaWZvcm5pYTEPMA0GA1UECgwGR29vZ2xlMQ0wCwYDVQQL +DARnUlBDMCAXDTE5MDYyNDIyMjIzM1oYDzIxMTkwNTMxMjIyMjMzWjBWMQswCQYD +VQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEPMA0GA1UECgwGR29vZ2xlMQ0w +CwYDVQQLDARnUlBDMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQCtCW0TjugnIUu8BEVIYvdMP+/2GENQDjZhZ8eKR5C6 +toDGbgjsDtt/GxISAg4cg70fIvy0XolnGPZodvfHDM4lJ7yHBOdZD8TXQoE6okR7 +HZuLUJ20M0pXgWqtRewKRUjuYsSDXBnzLiZw1dcv9nGpo+Bqa8NonpiGRRpEkshF +D6T9KU9Ts/x+wMQBIra2Gj0UMh79jPhUuxcYAQA0JQGivnOtdwuPiumpnUT8j8h6 +tWg5l01EsCZWJecCF85KnGpJEVYPyPqBqGsy0nGS9plGotOWF87+jyUQt+KD63xA +aBmTro86mKDDKEK4JvzjVeMGz2UbVcLPiiZnErTFaiXJAgMBAAEwDQYJKoZIhvcN +AQELBQADggEBAKsDgOPCWp5WCy17vJbRlgfgk05sVNIHZtzrmdswjBmvSg8MUpep +XqcPNUpsljAXsf9UM5IFEMRdilUsFGWvHjBEtNAW8WUK9UV18WRuU//0w1Mp5HAN +xUEKb4BoyZr65vlCnTR+AR5c9FfPvLibhr5qHs2RA8Y3GyLOcGqBWed87jhdQLCc +P1bxB+96le5JeXq0tw215lxonI2/3ZYVK4/ok9gwXrQoWm8YieJqitk/ZQ4S17/4 +pynHtDfdxLn23EXeGx+UTxJGfpRmhEZdJ+MN7QGYoomzx5qS5XoYKxRNrDlirJpr +OqXIn8E1it+6d5gOZfuHawcNGhRLplE/pfA= +-----END CERTIFICATE----- diff --git a/contrib/envoyproxy/envoy/cmd/serviceextensions/localhost.key b/contrib/envoyproxy/envoy/cmd/serviceextensions/localhost.key new file mode 100644 index 0000000000..72e2463282 --- /dev/null +++ b/contrib/envoyproxy/envoy/cmd/serviceextensions/localhost.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEArQltE47oJyFLvARFSGL3TD/v9hhDUA42YWfHikeQuraAxm4I +7A7bfxsSEgIOHIO9HyL8tF6JZxj2aHb3xwzOJSe8hwTnWQ/E10KBOqJEex2bi1Cd +tDNKV4FqrUXsCkVI7mLEg1wZ8y4mcNXXL/ZxqaPgamvDaJ6YhkUaRJLIRQ+k/SlP +U7P8fsDEASK2tho9FDIe/Yz4VLsXGAEANCUBor5zrXcLj4rpqZ1E/I/IerVoOZdN +RLAmViXnAhfOSpxqSRFWD8j6gahrMtJxkvaZRqLTlhfO/o8lELfig+t8QGgZk66P +OpigwyhCuCb841XjBs9lG1XCz4omZxK0xWolyQIDAQABAoIBADeq/Kh6JT3RfGf0 +h8WN8TlaqHxnueAbcmtL0+oss+cdp7gu1jf7X6o4r0uT1a5ew40s2Fe+wj2kzkE1 +ZOlouTlC22gkr7j7Vbxa7PBMG/Pvxoa/XL0IczZLsGImSJXVTG1E4SvRiZeulTdf +1GbdxhtpWV1jZe5Wd4Na3+SHxF5S7m3PrHiZlYdz1ND+8XZs1NlL9+ej72qSFul9 +t/QjMWJ9pky/Wad5abnRLRyOsg+BsgnXbkUy2rD89ZxFMLda9pzXo3TPyAlBHonr +mkEsE4eRMWMpjBM79JbeyDdHn/cs/LjAZrzeDf7ugXr2CHQpKaM5O0PsNHezJII9 +L5kCfzECgYEA4M/rz1UP1/BJoSqigUlSs0tPAg8a5UlkVsh6Osuq72IPNo8qg/Fw +oV/IiIS+q+obRcFj1Od3PGdTpCJwW5dzd2fXBQGmGdj0HucnCrs13RtBh91JiF5i +y/YYI9KfgOG2ZT9gG68T0gTs6jRrS3Qd83npqjrkJqMOd7s00MK9tUcCgYEAxQq7 +T541oCYHSBRIIb0IrR25krZy9caxzCqPDwOcuuhaCqCiaq+ATvOWlSfgecm4eH0K +PCH0xlWxG0auPEwm4pA8+/WR/XJwscPZMuoht1EoKy1his4eKx/s7hHNeO6KOF0V +Y/zqIiuZnEwUoKbn7EqqNFSTT65PJKyGsICJFG8CgYAfaw9yl1myfQNdQb8aQGwN +YJ33FLNWje427qeeZe5KrDKiFloDvI9YDjHRWnPnRL1w/zj7fSm9yFb5HlMDieP6 +MQnsyjEzdY2QcA+VwVoiv3dmDHgFVeOKy6bOAtaFxYWfGr9MvygO9t9BT/gawGyb +JVORlc9i0vDnrMMR1dV7awKBgBpTWLtGc/u1mPt0Wj7HtsUKV6TWY32a0l5owTxM +S0BdksogtBJ06DukJ9Y9wawD23WdnyRxlPZ6tHLkeprrwbY7dypioOKvy4a0l+xJ +g7+uRCOgqIuXBkjUtx8HmeAyXp0xMo5tWArAsIFFWOwt4IadYygitJvMuh44PraO +NcJZAoGADEiV0dheXUCVr8DrtSom8DQMj92/G/FIYjXL8OUhh0+F+YlYP0+F8PEU +yYIWEqL/S5tVKYshimUXQa537JcRKsTVJBG/ZKD2kuqgOc72zQy3oplimXeJDCXY +h2eAQ0u8GN6tN9C4t8Kp4a3y6FGsxgu+UTxdnL3YQ+yHAVhtCzo= +-----END RSA PRIVATE KEY----- diff --git a/contrib/envoyproxy/envoy/cmd/serviceextensions/main.go b/contrib/envoyproxy/envoy/cmd/serviceextensions/main.go new file mode 100644 index 0000000000..fcd86b8fb3 --- /dev/null +++ b/contrib/envoyproxy/envoy/cmd/serviceextensions/main.go @@ -0,0 +1,128 @@ +package main + +import ( + "crypto/tls" + "gopkg.in/DataDog/dd-trace-go.v1/contrib/envoyproxy/envoy" + "gopkg.in/DataDog/dd-trace-go.v1/internal/log" + "gopkg.in/DataDog/dd-trace-go.v1/internal/version" + "net" + "net/http" + "os" + + extproc "github.com/envoyproxy/go-control-plane/envoy/service/ext_proc/v3" + "github.com/gorilla/mux" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/reflection" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" +) + +// AppsecCalloutExtensionService defines the struct that follows the ExternalProcessorServer interface. +type AppsecCalloutExtensionService struct { + extproc.ExternalProcessorServer +} + +type serviceExtensionConfig struct { + extensionPort string + extensionHost string + healthcheckPort string +} + +func loadConfig() serviceExtensionConfig { + extensionPort := os.Getenv("DD_SERVICE_EXTENSION_PORT") + if extensionPort == "" { + extensionPort = "443" + } + + extensionHost := os.Getenv("DD_SERVICE_EXTENSION_HOST") + if extensionHost == "" { + extensionHost = "0.0.0.0" + } + + healthcheckPort := os.Getenv("DD_SERVICE_EXTENSION_HEALTHCHECK_PORT") + if healthcheckPort == "" { + healthcheckPort = "80" + } + + return serviceExtensionConfig{ + extensionPort: extensionPort, + extensionHost: extensionHost, + healthcheckPort: healthcheckPort, + } +} + +func main() { + var extensionService AppsecCalloutExtensionService + + // Force set ASM as enabled only if the environment variable is not set + // Note: If the environment variable is set to false, it should be disabled + if os.Getenv("DD_APPSEC_ENABLED") == "" { + if err := os.Setenv("DD_APPSEC_ENABLED", "1"); err != nil { + log.Error("service_extension: failed to set DD_APPSEC_ENABLED environment variable: %v\n", err) + } + } + + // TODO: Enable ASM standalone mode when it is developed (should be done for Q4 2024) + + // Set the DD_VERSION to the current tracer version if not set + if os.Getenv("DD_VERSION") == "" { + if err := os.Setenv("DD_VERSION", version.Tag); err != nil { + log.Error("service_extension: failed to set DD_VERSION environment variable: %v\n", err) + } + } + + config := loadConfig() + + tracer.Start() + + go StartGPRCSsl(&extensionService, config) + log.Info("service_extension: callout gRPC server started on %s:%s\n", config.extensionHost, config.extensionPort) + + go startHealthCheck(config) + log.Info("service_extension: health check server started on %s:%s\n", config.extensionHost, config.healthcheckPort) + + select {} +} + +func startHealthCheck(config serviceExtensionConfig) { + muxServer := mux.NewRouter() + muxServer.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, err := w.Write([]byte(`{"status": "ok", "library": {"language": "golang", "version": "` + version.Tag + `"}}`)) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + }) + + server := &http.Server{ + Addr: config.extensionHost + ":" + config.healthcheckPort, + Handler: muxServer, + } + + println(server.ListenAndServe()) +} + +func StartGPRCSsl(service extproc.ExternalProcessorServer, config serviceExtensionConfig) { + cert, err := tls.LoadX509KeyPair("localhost.crt", "localhost.key") + if err != nil { + log.Error("Failed to load key pair: %v\n", err) + } + + lis, err := net.Listen("tcp", config.extensionHost+":"+config.extensionPort) + if err != nil { + log.Error("Failed to listen: %v\n", err) + } + + si := envoy.StreamServerInterceptor() + creds := credentials.NewServerTLSFromCert(&cert) + grpcServer := grpc.NewServer(grpc.StreamInterceptor(si), grpc.Creds(creds)) + + extproc.RegisterExternalProcessorServer(grpcServer, service) + reflection.Register(grpcServer) + if err := grpcServer.Serve(lis); err != nil { + log.Error("service_extension: failed to serve gRPC: %v\n", err) + } +} diff --git a/contrib/envoyproxy/envoy/envoy.go b/contrib/envoyproxy/envoy/envoy.go new file mode 100644 index 0000000000..2002d171e3 --- /dev/null +++ b/contrib/envoyproxy/envoy/envoy.go @@ -0,0 +1,420 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016 Datadog, Inc. + +package envoy + +import ( + "context" + "errors" + "io" + "net/http" + "net/url" + "strconv" + "strings" + "sync/atomic" + + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + grpctrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/google.golang.org/grpc" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext" + "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/emitter/waf/actions" + "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/listener/trace" + + corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" + v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" + extproc "github.com/envoyproxy/go-control-plane/envoy/service/ext_proc/v3" + v32 "github.com/envoyproxy/go-control-plane/envoy/type/v3" + + "gopkg.in/DataDog/dd-trace-go.v1/contrib/internal/httptrace" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" + "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/emitter/httpsec" + httpsec2 "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/listener/httpsec" + "gopkg.in/DataDog/dd-trace-go.v1/internal/log" +) + +type CurrentRequest struct { + op *httpsec.HandlerOperation + blockAction *atomic.Pointer[actions.BlockHTTP] + span tracer.Span + + remoteAddr string + parsedUrl *url.URL + requestArgs httpsec.HandlerOperationArgs + + statusCode int + blocked bool +} + +func getRemoteAddr(xfwd []string) string { + length := len(xfwd) + if length == 0 { + return "" + } + + // Get the first right value of x-forwarded-for header + // The rightmost IP address is the one that will be used as the remote client IP + // https://datadoghq.atlassian.net/wiki/spaces/TS/pages/2766733526/Sensitive+IP+information#Where-does-the-value-of-the-http.client_ip-tag-come-from%3F + return xfwd[length-1] +} + +func StreamServerInterceptor(opts ...grpctrace.Option) grpc.StreamServerInterceptor { + interceptor := grpctrace.StreamServerInterceptor(opts...) + + return func(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { + if info.FullMethod != extproc.ExternalProcessor_Process_FullMethodName { + return interceptor(srv, ss, info, handler) + } + + ctx := ss.Context() + md, _ := metadata.FromIncomingContext(ctx) + currentRequest := &CurrentRequest{ + blocked: false, + remoteAddr: getRemoteAddr(md.Get("x-forwarded-for")), + } + + // Close the span when the request is done processing + defer func() { + closeSpan(currentRequest) + }() + + for { + select { + case <-ctx.Done(): + if errors.Is(ctx.Err(), context.Canceled) { + return nil + } + + return ctx.Err() + + default: + } + + var req extproc.ProcessingRequest + err := ss.RecvMsg(&req) + 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 { + return nil + } + + log.Warn("external_processing: error receiving request/response: %v\n", err) + return status.Errorf(codes.Unknown, "Error receiving request/response: %v", err) + } + + resp, err := envoyExternalProcessingEventHandler(ctx, &req, currentRequest) + if err != nil { + log.Error("external_processing: error processing request/response: %v\n", err) + return status.Errorf(codes.Unknown, "Error processing request/response: %v", err) + } + + // End of stream reached, no more data to process + if resp == nil { + log.Debug("external_processing: end of stream reached") + return nil + } + + // Send Message could fail if envoy close the stream before the message could be sent (probably because of an Envoy timeout) + if err := ss.SendMsg(resp); err != nil { + log.Warn("external_processing: error sending response (probably because of an Envoy timeout): %v", err) + return status.Errorf(codes.Unknown, "Error sending response (probably because of an Envoy timeout): %v", err) + } + + if currentRequest.blocked { + log.Debug("external_processing: request blocked, stream ended") + return nil + } + } + } +} + +func envoyExternalProcessingEventHandler(ctx context.Context, req *extproc.ProcessingRequest, currentRequest *CurrentRequest) (*extproc.ProcessingResponse, error) { + switch v := req.Request.(type) { + case *extproc.ProcessingRequest_RequestHeaders: + return ProcessRequestHeaders(ctx, req.Request.(*extproc.ProcessingRequest_RequestHeaders), currentRequest) + + case *extproc.ProcessingRequest_RequestBody: + // TODO: Handle request raw body in the WAF + return &extproc.ProcessingResponse{ + Response: &extproc.ProcessingResponse_RequestBody{ + RequestBody: &extproc.BodyResponse{ + Response: &extproc.CommonResponse{ + Status: extproc.CommonResponse_CONTINUE, + }, + }, + }, + }, nil + + case *extproc.ProcessingRequest_RequestTrailers: + return &extproc.ProcessingResponse{ + Response: &extproc.ProcessingResponse_RequestTrailers{}, + }, nil + + case *extproc.ProcessingRequest_ResponseHeaders: + return ProcessResponseHeaders(req.Request.(*extproc.ProcessingRequest_ResponseHeaders), currentRequest) + + case *extproc.ProcessingRequest_ResponseBody: + r := req.Request.(*extproc.ProcessingRequest_ResponseBody) + + // Note: The end of stream bool value is not reliable + // Sometimes it's not set to true even if there is no more data to process + if r.ResponseBody.GetEndOfStream() { + return nil, nil + } + + // TODO: Handle response raw body in the WAF + return &extproc.ProcessingResponse{ + Response: &extproc.ProcessingResponse_ResponseBody{}, + }, nil + + case *extproc.ProcessingRequest_ResponseTrailers: + return &extproc.ProcessingResponse{ + Response: &extproc.ProcessingResponse_RequestTrailers{}, + }, nil + + default: + return nil, status.Errorf(codes.Unknown, "Unknown request type: %T", v) + } +} + +func ProcessRequestHeaders(ctx context.Context, req *extproc.ProcessingRequest_RequestHeaders, currentRequest *CurrentRequest) (*extproc.ProcessingResponse, error) { + log.Debug("external_processing: received request headers: %v\n", req.RequestHeaders) + + headers, envoyHeaders := separateEnvoyHeaders(req.RequestHeaders.GetHeaders().GetHeaders()) + + // Create args + host, scheme, path, method, err := verifyRequestHttp2RequestHeaders(envoyHeaders) + if err != nil { + return nil, err + } + + requestURI := scheme + "://" + host + path + parsedUrl, err := url.Parse(requestURI) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "Error parsing request URI: %v", err) + } + currentRequest.parsedUrl = parsedUrl + + // client ip set in the x-forwarded-for header (cf: https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_conn_man/headers#x-forwarded-for) + ipTags, _ := httpsec2.ClientIPTags(headers, true, currentRequest.remoteAddr) + + currentRequest.requestArgs = httpsec.MakeHandlerOperationArgs(headers, method, host, currentRequest.remoteAddr, parsedUrl) + headers = currentRequest.requestArgs.Headers // Replace headers with the ones from the args because it has been modified + + // Create span + currentRequest.span = createExternalProcessedSpan(ctx, headers, method, host, path, currentRequest.remoteAddr, ipTags, parsedUrl) + + // Run WAF on request data + currentRequest.op, currentRequest.blockAction, _ = httpsec.StartOperation(ctx, currentRequest.requestArgs) + + // Block handling: If triggered, we need to block the request, return an immediate response + if blockPtr := currentRequest.blockAction.Load(); blockPtr != nil { + response := doBlockRequest(currentRequest, blockPtr, headers) + return response, nil + } + + return &extproc.ProcessingResponse{ + Response: &extproc.ProcessingResponse_RequestHeaders{ + RequestHeaders: &extproc.HeadersResponse{ + Response: &extproc.CommonResponse{ + Status: extproc.CommonResponse_CONTINUE, + }, + }, + }, + }, nil +} + +// Verify the required HTTP2 headers are present +// Some mandatory headers need to be set. It can happen when it wasn't a real HTTP2 request sent by Envoy, +func verifyRequestHttp2RequestHeaders(headers map[string][]string) (string, string, string, string, error) { + // :authority, :scheme, :path, :method + + for _, header := range []string{":authority", ":scheme", ":path", ":method"} { + if _, ok := headers[header]; !ok { + return "", "", "", "", status.Errorf(codes.InvalidArgument, "Missing required header: %v", header) + } + } + + return headers[":authority"][0], headers[":scheme"][0], headers[":path"][0], headers[":method"][0], nil +} + +func verifyRequestHttp2ResponseHeaders(headers map[string][]string) (string, error) { + // :status + + if _, ok := headers[":status"]; !ok { + return "", status.Errorf(codes.InvalidArgument, "Missing required header: %v", ":status") + } + + return headers[":status"][0], nil +} + +func ProcessResponseHeaders(res *extproc.ProcessingRequest_ResponseHeaders, currentRequest *CurrentRequest) (*extproc.ProcessingResponse, error) { + log.Debug("external_processing: received response headers: %v\n", res.ResponseHeaders) + + headers, envoyHeaders := separateEnvoyHeaders(res.ResponseHeaders.GetHeaders().GetHeaders()) + + statusCodeStr, err := verifyRequestHttp2ResponseHeaders(envoyHeaders) + if err != nil { + return nil, err + } + + currentRequest.statusCode, err = strconv.Atoi(statusCodeStr) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "Error parsing response header status code: %v", err) + } + + args := httpsec.HandlerOperationRes{ + Headers: headers, + StatusCode: currentRequest.statusCode, + } + + currentRequest.op.Finish(args, currentRequest.span) + currentRequest.op = nil + + // Block handling: If triggered, we need to block the request, return an immediate response + if blockPtr := currentRequest.blockAction.Load(); blockPtr != nil { + return doBlockRequest(currentRequest, blockPtr, headers), nil + } + + httpsec2.SetResponseHeadersTags(currentRequest.span, headers) + + // Note: (cf. comment in the stream error handling) + // The end of stream bool value is not reliable + if res.ResponseHeaders.GetEndOfStream() { + return nil, nil + } + + return &extproc.ProcessingResponse{ + Response: &extproc.ProcessingResponse_ResponseHeaders{ + ResponseHeaders: &extproc.HeadersResponse{ + Response: &extproc.CommonResponse{ + Status: extproc.CommonResponse_CONTINUE, + }, + }, + }, + }, nil +} + +func createExternalProcessedSpan(ctx context.Context, headers map[string][]string, method string, host string, path string, remoteAddr string, ipTags map[string]string, parsedUrl *url.URL) tracer.Span { + userAgent := "" + if ua, ok := headers["User-Agent"]; ok { + userAgent = ua[0] + } + + span, _ := httptrace.StartHttpSpan( + ctx, + headers, + host, + method, + httptrace.UrlFromUrl(parsedUrl), + userAgent, + remoteAddr, + []ddtrace.StartSpanOption{ + func(cfg *ddtrace.StartSpanConfig) { + cfg.Tags[ext.ResourceName] = method + " " + path + cfg.Tags[ext.SpanKind] = ext.SpanKindServer + + // Add client IP tags + for k, v := range ipTags { + cfg.Tags[k] = v + } + }, + }..., + ) + + httpsec2.SetRequestHeadersTags(span, headers) + trace.SetAppsecStaticTags(span) + + return span +} + +// Separate normal headers of the initial request made by the client and the pseudo headers of HTTP/2 +// - Format the headers to be used by the tracer as a map[string][]string +// - Set header keys to be canonical +func separateEnvoyHeaders(receivedHeaders []*corev3.HeaderValue) (map[string][]string, map[string][]string) { + headers := make(map[string][]string) + pseudoHeadersHttp2 := make(map[string][]string) + for _, v := range receivedHeaders { + key := v.GetKey() + if key[0] == ':' { + pseudoHeadersHttp2[key] = []string{string(v.GetRawValue())} + } else { + headers[http.CanonicalHeaderKey(key)] = []string{string(v.GetRawValue())} + } + } + return headers, pseudoHeadersHttp2 +} + +func doBlockRequest(currentRequest *CurrentRequest, blockAction *actions.BlockHTTP, headers map[string][]string) *extproc.ProcessingResponse { + currentRequest.blocked = true + + var headerToSet map[string][]string + var body []byte + if blockAction.RedirectLocation != "" { + headerToSet, body = actions.HandleRedirectLocationString( + currentRequest.parsedUrl.Path, + blockAction.RedirectLocation, + blockAction.StatusCode, + currentRequest.requestArgs.Method, + currentRequest.requestArgs.Headers, + ) + } else { + headerToSet, body = blockAction.BlockingTemplate(headers) + } + + var headersMutation []*v3.HeaderValueOption + for k, v := range headerToSet { + headersMutation = append(headersMutation, &v3.HeaderValueOption{ + Header: &v3.HeaderValue{ + Key: k, + RawValue: []byte(strings.Join(v, ",")), + }, + }) + } + + httpsec2.SetResponseHeadersTags(currentRequest.span, headerToSet) + currentRequest.statusCode = blockAction.StatusCode + + return &extproc.ProcessingResponse{ + Response: &extproc.ProcessingResponse_ImmediateResponse{ + ImmediateResponse: &extproc.ImmediateResponse{ + Status: &v32.HttpStatus{ + Code: v32.StatusCode(currentRequest.statusCode), + }, + Headers: &extproc.HeaderMutation{ + SetHeaders: headersMutation, + }, + Body: body, + GrpcStatus: &extproc.GrpcStatus{ + Status: 0, + }, + }, + }, + } +} + +func closeSpan(currentRequest *CurrentRequest) { + span := currentRequest.span + if span != nil { + // Finish the operation: it can be not finished when the request has been blocked or if an error occurred + // > The response hasn't been processed + if currentRequest.op != nil { + currentRequest.op.Finish(httpsec.HandlerOperationRes{}, span) + currentRequest.op = nil + } + + // Note: The status code could be 0 if an internal error occurred + statusCodeStr := strconv.Itoa(currentRequest.statusCode) + span.SetTag(ext.HTTPCode, statusCodeStr) + + span.Finish() + + log.Debug("external_processing: span closed with status code: %v\n", currentRequest.statusCode) + currentRequest.span = nil + } +} diff --git a/contrib/envoyproxy/envoy/envoy_test.go b/contrib/envoyproxy/envoy/envoy_test.go new file mode 100644 index 0000000000..e3f872f87f --- /dev/null +++ b/contrib/envoyproxy/envoy/envoy_test.go @@ -0,0 +1,569 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016 Datadog, Inc. + +// TODO: Blocking and Redirect action to test + +package envoy + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net" + "testing" + + extproc "github.com/envoyproxy/go-control-plane/envoy/service/ext_proc/v3" + typev3 "github.com/envoyproxy/go-control-plane/envoy/type/v3" + + ddgrpc "gopkg.in/DataDog/dd-trace-go.v1/contrib/google.golang.org/grpc" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/mocktracer" + "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec" + + v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" +) + +func end2EndStreamRequest(t *testing.T, stream extproc.ExternalProcessor_ProcessClient, path string, method string, requestHeaders map[string]string, responseHeaders map[string]string, blockOnResponse bool) { + // First part: request + // 1- Send the headers + err := stream.Send(&extproc.ProcessingRequest{ + Request: &extproc.ProcessingRequest_RequestHeaders{ + RequestHeaders: &extproc.HttpHeaders{ + Headers: makeRequestHeaders(requestHeaders, method, path), + }, + }, + }) + require.NoError(t, err) + + res, err := stream.Recv() + require.NoError(t, err) + require.Equal(t, extproc.CommonResponse_CONTINUE, res.GetRequestHeaders().GetResponse().GetStatus()) + + // 2- Send the body + err = stream.Send(&extproc.ProcessingRequest{ + Request: &extproc.ProcessingRequest_RequestBody{ + RequestBody: &extproc.HttpBody{ + Body: []byte("body"), + }, + }, + }) + require.NoError(t, err) + + res, err = stream.Recv() + require.NoError(t, err) + require.Equal(t, extproc.CommonResponse_CONTINUE, res.GetRequestBody().GetResponse().GetStatus()) + + // 3- Send the trailers + err = stream.Send(&extproc.ProcessingRequest{ + Request: &extproc.ProcessingRequest_RequestTrailers{ + RequestTrailers: &extproc.HttpTrailers{ + Trailers: &v3.HeaderMap{ + Headers: []*v3.HeaderValue{ + {Key: "key", Value: "value"}, + }, + }, + }, + }, + }) + require.NoError(t, err) + + res, err = stream.Recv() + require.NoError(t, err) + require.NotNil(t, res.GetRequestTrailers()) + + // Second part: response + // 1- Send the response headers + err = stream.Send(&extproc.ProcessingRequest{ + Request: &extproc.ProcessingRequest_ResponseHeaders{ + ResponseHeaders: &extproc.HttpHeaders{ + Headers: makeResponseHeaders(responseHeaders, "200"), + }, + }, + }) + require.NoError(t, err) + + if blockOnResponse { + // Should have received an immediate response for blocking + // Let the test handle the response + return + } + + res, err = stream.Recv() + require.NoError(t, err) + require.Equal(t, extproc.CommonResponse_CONTINUE, res.GetResponseHeaders().GetResponse().GetStatus()) + + // 2- Send the response body + err = stream.Send(&extproc.ProcessingRequest{ + Request: &extproc.ProcessingRequest_ResponseBody{ + ResponseBody: &extproc.HttpBody{ + Body: []byte("body"), + EndOfStream: true, + }, + }, + }) + require.NoError(t, err) + + // The stream should now be closed + _, err = stream.Recv() + require.Equal(t, io.EOF, err) +} + +func checkForAppsecEvent(t *testing.T, finished []mocktracer.Span, expectedRuleIDs map[string]int) { + // The request should have the attack attempts + event := finished[len(finished)-1].Tag("_dd.appsec.json") + require.NotNil(t, event, "the _dd.appsec.json tag was not found") + + jsonText := event.(string) + type trigger struct { + Rule struct { + ID string `json:"id"` + } `json:"rule"` + } + var parsed struct { + Triggers []trigger `json:"triggers"` + } + err := json.Unmarshal([]byte(jsonText), &parsed) + require.NoError(t, err) + + histogram := map[string]uint8{} + for _, tr := range parsed.Triggers { + histogram[tr.Rule.ID]++ + } + + for ruleID, count := range expectedRuleIDs { + require.Equal(t, count, int(histogram[ruleID]), "rule %s has been triggered %d times but expected %d") + } + + require.Len(t, parsed.Triggers, len(expectedRuleIDs), "unexpected number of rules triggered") +} + +func TestAppSec(t *testing.T) { + appsec.Start() + defer appsec.Stop() + if !appsec.Enabled() { + t.Skip("appsec disabled") + } + + setup := func() (extproc.ExternalProcessorClient, mocktracer.Tracer, func()) { + rig, err := newEnvoyAppsecRig(false) + require.NoError(t, err) + + mt := mocktracer.Start() + + return rig.client, mt, func() { + rig.Close() + mt.Stop() + } + } + + t.Run("monitoring-event-on-request", func(t *testing.T) { + client, mt, cleanup := setup() + defer cleanup() + + ctx := context.Background() + stream, err := client.Process(ctx) + require.NoError(t, err) + + end2EndStreamRequest(t, stream, "/", "GET", map[string]string{"User-Agent": "dd-test-scanner-log"}, map[string]string{}, false) + + err = stream.CloseSend() + require.NoError(t, err) + stream.Recv() // to flush the spans + + finished := mt.FinishedSpans() + require.Len(t, finished, 1) + checkForAppsecEvent(t, finished, map[string]int{"ua0-600-55x": 1}) + }) + + t.Run("blocking-event-on-request", func(t *testing.T) { + client, mt, cleanup := setup() + defer cleanup() + + ctx := context.Background() + stream, err := client.Process(ctx) + require.NoError(t, err) + + err = stream.Send(&extproc.ProcessingRequest{ + Request: &extproc.ProcessingRequest_RequestHeaders{ + RequestHeaders: &extproc.HttpHeaders{ + Headers: makeRequestHeaders(map[string]string{"User-Agent": "dd-test-scanner-log-block"}, "GET", "/"), + }, + }, + }) + require.NoError(t, err) + + res, err := stream.Recv() + require.Equal(t, uint32(0), res.GetImmediateResponse().GetGrpcStatus().Status) + require.Equal(t, typev3.StatusCode(403), res.GetImmediateResponse().GetStatus().Code) + require.Equal(t, "Content-Type", res.GetImmediateResponse().GetHeaders().SetHeaders[0].GetHeader().Key) + require.Equal(t, "application/json", string(res.GetImmediateResponse().GetHeaders().SetHeaders[0].GetHeader().RawValue)) + require.NoError(t, err) + + err = stream.CloseSend() + require.NoError(t, err) + stream.Recv() // to flush the spans + + finished := mt.FinishedSpans() + require.Len(t, finished, 1) + checkForAppsecEvent(t, finished, map[string]int{"ua0-600-56x": 1}) + + // Check for tags + span := finished[0] + require.Equal(t, true, span.Tag("appsec.event")) + require.Equal(t, true, span.Tag("appsec.blocked")) + }) +} + +func TestBlockingWithUserRulesFile(t *testing.T) { + t.Setenv("DD_APPSEC_RULES", "../../../internal/appsec/testdata/user_rules.json") + appsec.Start() + defer appsec.Stop() + if !appsec.Enabled() { + t.Skip("appsec disabled") + } + + setup := func() (extproc.ExternalProcessorClient, mocktracer.Tracer, func()) { + rig, err := newEnvoyAppsecRig(false) + require.NoError(t, err) + + mt := mocktracer.Start() + + return rig.client, mt, func() { + rig.Close() + mt.Stop() + } + } + + t.Run("blocking-event-on-response", func(t *testing.T) { + client, mt, cleanup := setup() + defer cleanup() + + ctx := context.Background() + stream, err := client.Process(ctx) + require.NoError(t, err) + + end2EndStreamRequest(t, stream, "/", "OPTION", map[string]string{"User-Agent": "dd-test-scanner-log-block"}, map[string]string{"User-Agent": "match-response-header"}, true) + + // Handle the immediate response + res, err := stream.Recv() + require.Equal(t, uint32(0), res.GetImmediateResponse().GetGrpcStatus().Status) + require.Equal(t, typev3.StatusCode(418), res.GetImmediateResponse().GetStatus().Code) // 418 because of the rule file + require.Equal(t, "Content-Type", res.GetImmediateResponse().GetHeaders().SetHeaders[0].GetHeader().Key) + require.Equal(t, "application/json", string(res.GetImmediateResponse().GetHeaders().SetHeaders[0].GetHeader().RawValue)) + require.NoError(t, err) + + err = stream.CloseSend() + require.NoError(t, err) + stream.Recv() // to flush the spans + + finished := mt.FinishedSpans() + require.Len(t, finished, 1) + checkForAppsecEvent(t, finished, map[string]int{"headers-003": 1}) + + // Check for tags + span := finished[0] + require.Equal(t, 1, span.Tag("_dd.appsec.enabled")) + require.Equal(t, true, span.Tag("appsec.event")) + require.Equal(t, true, span.Tag("appsec.blocked")) + }) + + t.Run("blocking-event-on-request-on-query", func(t *testing.T) { + client, mt, cleanup := setup() + defer cleanup() + + ctx := context.Background() + stream, err := client.Process(ctx) + require.NoError(t, err) + + err = stream.Send(&extproc.ProcessingRequest{ + Request: &extproc.ProcessingRequest_RequestHeaders{ + RequestHeaders: &extproc.HttpHeaders{ + Headers: makeRequestHeaders(map[string]string{"User-Agent": "Mistake Not..."}, "GET", "/hello?match=match-request-query"), + }, + }, + }) + require.NoError(t, err) + + res, err := stream.Recv() + require.Equal(t, uint32(0), res.GetImmediateResponse().GetGrpcStatus().Status) + require.Equal(t, typev3.StatusCode(418), res.GetImmediateResponse().GetStatus().Code) + require.Equal(t, "Content-Type", res.GetImmediateResponse().GetHeaders().SetHeaders[0].GetHeader().Key) + require.Equal(t, "application/json", string(res.GetImmediateResponse().GetHeaders().SetHeaders[0].GetHeader().RawValue)) + require.NoError(t, err) + + err = stream.CloseSend() + require.NoError(t, err) + stream.Recv() // to flush the spans + + finished := mt.FinishedSpans() + require.Len(t, finished, 1) + checkForAppsecEvent(t, finished, map[string]int{"query-002": 1}) + + // Check for tags + span := finished[0] + require.Equal(t, true, span.Tag("appsec.event")) + require.Equal(t, true, span.Tag("appsec.blocked")) + }) + + t.Run("blocking-event-on-request-on-cookies", func(t *testing.T) { + client, mt, cleanup := setup() + defer cleanup() + + ctx := context.Background() + stream, err := client.Process(ctx) + require.NoError(t, err) + + err = stream.Send(&extproc.ProcessingRequest{ + Request: &extproc.ProcessingRequest_RequestHeaders{ + RequestHeaders: &extproc.HttpHeaders{ + Headers: makeRequestHeaders(map[string]string{"Cookie": "foo=jdfoSDGFkivRG_234"}, "OPTIONS", "/"), + }, + }, + }) + require.NoError(t, err) + + res, err := stream.Recv() + require.Equal(t, uint32(0), res.GetImmediateResponse().GetGrpcStatus().Status) + require.Equal(t, typev3.StatusCode(418), res.GetImmediateResponse().GetStatus().Code) + require.Equal(t, "Content-Type", res.GetImmediateResponse().GetHeaders().SetHeaders[0].GetHeader().Key) + require.Equal(t, "application/json", string(res.GetImmediateResponse().GetHeaders().SetHeaders[0].GetHeader().RawValue)) + require.NoError(t, err) + + err = stream.CloseSend() + require.NoError(t, err) + stream.Recv() // to flush the spans + + finished := mt.FinishedSpans() + require.Len(t, finished, 1) + checkForAppsecEvent(t, finished, map[string]int{"tst-037-008": 1}) + + // Check for tags + span := finished[0] + require.Equal(t, true, span.Tag("appsec.event")) + require.Equal(t, true, span.Tag("appsec.blocked")) + }) +} + +func TestGeneratedSpan(t *testing.T) { + setup := func() (extproc.ExternalProcessorClient, mocktracer.Tracer, func()) { + rig, err := newEnvoyAppsecRig(false) + require.NoError(t, err) + + mt := mocktracer.Start() + + return rig.client, mt, func() { + rig.Close() + mt.Stop() + } + } + + t.Run("request-span", func(t *testing.T) { + client, mt, cleanup := setup() + defer cleanup() + + ctx := context.Background() + stream, err := client.Process(ctx) + require.NoError(t, err) + + end2EndStreamRequest(t, stream, "/resource-span", "GET", map[string]string{"user-agent": "Mistake Not...", "test-key": "test-value"}, map[string]string{"response-test-key": "response-test-value"}, false) + + err = stream.CloseSend() + require.NoError(t, err) + stream.Recv() // to flush the spans + + finished := mt.FinishedSpans() + require.Len(t, finished, 1) + + // Check for tags + span := finished[0] + require.Equal(t, "http.request", span.OperationName()) + require.Equal(t, "https://datadoghq.com/resource-span", span.Tag("http.url")) + require.Equal(t, "GET", span.Tag("http.method")) + require.Equal(t, "datadoghq.com", span.Tag("http.host")) + require.Equal(t, "GET /resource-span", span.Tag("resource.name")) + require.Equal(t, "datadoghq.com", span.Tag("http.request.headers.host")) + require.Equal(t, "server", span.Tag("span.kind")) + require.Equal(t, "Mistake Not...", span.Tag("http.useragent")) + }) +} + +func TestXForwardedForHeaderClientIp(t *testing.T) { + t.Setenv("DD_APPSEC_RULES", "../../../internal/appsec/testdata/blocking.json") + appsec.Start() + defer appsec.Stop() + if !appsec.Enabled() { + t.Skip("appsec disabled") + } + + setup := func() (extproc.ExternalProcessorClient, mocktracer.Tracer, func()) { + rig, err := newEnvoyAppsecRig(false) + require.NoError(t, err) + + mt := mocktracer.Start() + + return rig.client, mt, func() { + rig.Close() + mt.Stop() + } + } + + t.Run("client-ip", func(t *testing.T) { + client, mt, cleanup := setup() + defer cleanup() + + ctx := context.Background() + stream, err := client.Process(ctx) + require.NoError(t, err) + + end2EndStreamRequest(t, stream, "/", "OPTION", + map[string]string{"User-Agent": "Mistake not...", "X-Forwarded-For": "18.18.18.18"}, + map[string]string{"User-Agent": "match-response-header"}, + true) + + err = stream.CloseSend() + require.NoError(t, err) + stream.Recv() // to flush the spans + + finished := mt.FinishedSpans() + require.Len(t, finished, 1) + + // Check for tags + span := finished[0] + require.Equal(t, "18.18.18.18", span.Tag("http.client_ip")) + + // Appsec + require.Equal(t, 1, span.Tag("_dd.appsec.enabled")) + }) + + t.Run("blocking-client-ip", func(t *testing.T) { + client, mt, cleanup := setup() + defer cleanup() + + ctx := context.Background() + stream, err := client.Process(ctx) + require.NoError(t, err) + + err = stream.Send(&extproc.ProcessingRequest{ + Request: &extproc.ProcessingRequest_RequestHeaders{ + RequestHeaders: &extproc.HttpHeaders{ + Headers: makeRequestHeaders(map[string]string{"User-Agent": "Mistake not...", "X-Forwarded-For": "1.2.3.4"}, "GET", "/"), + }, + }, + }) + require.NoError(t, err) + + // Handle the immediate response + res, err := stream.Recv() + require.Equal(t, uint32(0), res.GetImmediateResponse().GetGrpcStatus().Status) + require.Equal(t, typev3.StatusCode(403), res.GetImmediateResponse().GetStatus().Code) + require.Equal(t, "Content-Type", res.GetImmediateResponse().GetHeaders().SetHeaders[0].GetHeader().Key) + require.Equal(t, "application/json", string(res.GetImmediateResponse().GetHeaders().SetHeaders[0].GetHeader().RawValue)) + require.NoError(t, err) + + err = stream.CloseSend() + require.NoError(t, err) + stream.Recv() // to flush the spans + + finished := mt.FinishedSpans() + require.Len(t, finished, 1) + checkForAppsecEvent(t, finished, map[string]int{"blk-001-001": 1}) + + // Check for tags + span := finished[0] + require.Equal(t, "1.2.3.4", span.Tag("http.client_ip")) + require.Equal(t, 1, span.Tag("_dd.appsec.enabled")) + require.Equal(t, true, span.Tag("appsec.event")) + require.Equal(t, true, span.Tag("appsec.blocked")) + }) +} + +func newEnvoyAppsecRig(traceClient bool, interceptorOpts ...ddgrpc.Option) (*envoyAppsecRig, error) { + interceptorOpts = append([]ddgrpc.InterceptorOption{ddgrpc.WithServiceName("grpc")}, interceptorOpts...) + + server := grpc.NewServer( + grpc.StreamInterceptor(StreamServerInterceptor(interceptorOpts...)), + ) + + fixtureServer := new(envoyFixtureServer) + extproc.RegisterExternalProcessorServer(server, fixtureServer) + + li, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + return nil, err + } + _, port, _ := net.SplitHostPort(li.Addr().String()) + // start our test fixtureServer. + go server.Serve(li) + + opts := []grpc.DialOption{grpc.WithInsecure()} + if traceClient { + opts = append(opts, + grpc.WithStreamInterceptor(ddgrpc.StreamClientInterceptor(interceptorOpts...)), + ) + } + conn, err := grpc.Dial(li.Addr().String(), opts...) + if err != nil { + return nil, fmt.Errorf("error dialing: %s", err) + } + return &envoyAppsecRig{ + fixtureServer: fixtureServer, + listener: li, + port: port, + server: server, + conn: conn, + client: extproc.NewExternalProcessorClient(conn), + }, err +} + +// rig contains all servers and connections we'd need for a grpc integration test +type envoyAppsecRig struct { + fixtureServer *envoyFixtureServer + server *grpc.Server + port string + listener net.Listener + conn *grpc.ClientConn + client extproc.ExternalProcessorClient +} + +func (r *envoyAppsecRig) Close() { + r.server.Stop() + r.conn.Close() +} + +type envoyFixtureServer struct { + extproc.ExternalProcessorServer +} + +// Helper functions + +// Construct request headers +func makeRequestHeaders(headers map[string]string, method string, path string) *v3.HeaderMap { + h := &v3.HeaderMap{} + for k, v := range headers { + h.Headers = append(h.Headers, &v3.HeaderValue{Key: k, RawValue: []byte(v)}) + } + + h.Headers = append(h.Headers, + &v3.HeaderValue{Key: ":method", RawValue: []byte(method)}, + &v3.HeaderValue{Key: ":path", RawValue: []byte(path)}, + &v3.HeaderValue{Key: ":scheme", RawValue: []byte("https")}, + &v3.HeaderValue{Key: ":authority", RawValue: []byte("datadoghq.com")}, + ) + + return h +} + +func makeResponseHeaders(headers map[string]string, status string) *v3.HeaderMap { + h := &v3.HeaderMap{} + for k, v := range headers { + h.Headers = append(h.Headers, &v3.HeaderValue{Key: k, RawValue: []byte(v)}) + } + + h.Headers = append(h.Headers, &v3.HeaderValue{Key: ":status", RawValue: []byte(status)}) + + return h +} diff --git a/contrib/internal/httptrace/httptrace.go b/contrib/internal/httptrace/httptrace.go index 4aad5aba38..0c8a848b70 100644 --- a/contrib/internal/httptrace/httptrace.go +++ b/contrib/internal/httptrace/httptrace.go @@ -11,6 +11,7 @@ import ( "context" "fmt" "net/http" + "net/url" "strconv" "strings" @@ -27,14 +28,29 @@ var ( ) // StartRequestSpan starts an HTTP request span with the standard list of HTTP request span tags (http.method, http.url, -// http.useragent). Any further span start option can be added with opts. +// http.useragent) with a http.Request object. Any further span start option can be added with opts. func StartRequestSpan(r *http.Request, opts ...ddtrace.StartSpanOption) (tracer.Span, context.Context) { + return StartHttpSpan( + r.Context(), + r.Header, + r.Host, + r.Method, + urlFromRequest(r), + r.UserAgent(), + r.RemoteAddr, + opts..., + ) +} + +// StartHttpSpan starts an HTTP request span with the standard list of HTTP request span tags (http.method, http.url, +// http.useragent). Any further span start option can be added with opts. +func StartHttpSpan(ctx context.Context, headers map[string][]string, host string, method string, url string, userAgent string, remoteAddr string, opts ...ddtrace.StartSpanOption) (tracer.Span, context.Context) { // Append our span options before the given ones so that the caller can "overwrite" them. // TODO(): rework span start option handling (https://github.com/DataDog/dd-trace-go/issues/1352) var ipTags map[string]string if cfg.traceClientIP { - ipTags, _ = httpsec.ClientIPTags(r.Header, true, r.RemoteAddr) + ipTags, _ = httpsec.ClientIPTags(headers, true, remoteAddr) } nopts := make([]ddtrace.StartSpanOption, 0, len(opts)+1+len(ipTags)) nopts = append(nopts, @@ -43,12 +59,12 @@ func StartRequestSpan(r *http.Request, opts ...ddtrace.StartSpanOption) (tracer. cfg.Tags = make(map[string]interface{}) } cfg.Tags[ext.SpanType] = ext.SpanTypeWeb - cfg.Tags[ext.HTTPMethod] = r.Method - cfg.Tags[ext.HTTPURL] = urlFromRequest(r) - cfg.Tags[ext.HTTPUserAgent] = r.UserAgent() + cfg.Tags[ext.HTTPMethod] = method + cfg.Tags[ext.HTTPURL] = url + cfg.Tags[ext.HTTPUserAgent] = userAgent cfg.Tags["_dd.measured"] = 1 - if r.Host != "" { - cfg.Tags["http.host"] = r.Host + if host != "" { + cfg.Tags["http.host"] = host } if spanctx, err := tracer.Extract(tracer.HTTPHeadersCarrier(r.Header)); err == nil { // If there are span links as a result of context extraction, add them as a StartSpanOption @@ -57,12 +73,13 @@ func StartRequestSpan(r *http.Request, opts ...ddtrace.StartSpanOption) (tracer. } tracer.ChildOf(spanctx)(cfg) } + for k, v := range ipTags { cfg.Tags[k] = v } }) nopts = append(nopts, opts...) - return tracer.StartSpanFromContext(r.Context(), namingschema.OpName(namingschema.HTTPServer), nopts...) + return tracer.StartSpanFromContext(ctx, namingschema.OpName(namingschema.HTTPServer), nopts...) } // FinishRequestSpan finishes the given HTTP request span and sets the expected response-related tags such as the status @@ -101,27 +118,54 @@ func urlFromRequest(r *http.Request) string { // "For most requests, fields other than Path and RawQuery will be // empty. (See RFC 7230, Section 5.3)" // This is why we don't rely on url.URL.String(), url.URL.Host, url.URL.Scheme, etc... - var url string - path := r.URL.EscapedPath() scheme := "http" if r.TLS != nil { scheme = "https" } - if r.Host != "" { - url = strings.Join([]string{scheme, "://", r.Host, path}, "") + + return urlFromArgs( + r.URL.EscapedPath(), + scheme, + r.Host, + r.URL.RawQuery, + r.URL.EscapedFragment(), + ) +} + +// UrlFromUrl returns the full URL from a URL object. If query params are collected, they are obfuscated granted +// obfuscation is not disabled by the user (through DD_TRACE_OBFUSCATION_QUERY_STRING_REGEXP) +// See https://docs.datadoghq.com/tracing/configure_data_security#redacting-the-query-in-the-url for more information. +func UrlFromUrl(u *url.URL) string { + scheme := "http" + if u.Scheme != "" { + scheme = u.Scheme + } + return urlFromArgs( + u.EscapedPath(), + scheme, + u.Host, + u.RawQuery, + u.EscapedFragment(), + ) +} + +func urlFromArgs(escapedPath string, scheme string, host string, rawQuery string, escapedFragment string) string { + var url string + if host != "" { + url = strings.Join([]string{scheme, "://", host, escapedPath}, "") } else { - url = path + url = escapedPath } // Collect the query string if we are allowed to report it and obfuscate it if possible/allowed - if cfg.queryString && r.URL.RawQuery != "" { - query := r.URL.RawQuery + if cfg.queryString && rawQuery != "" { + query := rawQuery if cfg.queryStringRegexp != nil { query = cfg.queryStringRegexp.ReplaceAllLiteralString(query, "") } url = strings.Join([]string{url, query}, "?") } - if frag := r.URL.EscapedFragment(); frag != "" { - url = strings.Join([]string{url, frag}, "#") + if escapedFragment != "" { + url = strings.Join([]string{url, escapedFragment}, "#") } return url } diff --git a/go.mod b/go.mod index 6696dcc4cc..c28ee16637 100644 --- a/go.mod +++ b/go.mod @@ -40,6 +40,7 @@ require ( github.com/elastic/go-elasticsearch/v8 v8.4.0 github.com/emicklei/go-restful v2.16.0+incompatible github.com/emicklei/go-restful/v3 v3.11.0 + github.com/envoyproxy/go-control-plane v0.13.0 github.com/garyburd/redigo v1.6.4 github.com/gin-gonic/gin v1.9.1 github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8 @@ -98,12 +99,12 @@ require ( go.opentelemetry.io/otel/trace v1.27.0 go.uber.org/goleak v1.3.0 golang.org/x/mod v0.20.0 - golang.org/x/oauth2 v0.18.0 + golang.org/x/oauth2 v0.20.0 golang.org/x/sys v0.24.0 golang.org/x/time v0.6.0 golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 google.golang.org/api v0.169.0 - google.golang.org/grpc v1.64.0 + google.golang.org/grpc v1.65.0 google.golang.org/protobuf v1.34.2 gopkg.in/jinzhu/gorm.v1 v1.9.2 gopkg.in/olivere/elastic.v3 v3.0.75 @@ -120,7 +121,7 @@ require ( require ( cloud.google.com/go v0.112.1 // indirect cloud.google.com/go/compute v1.25.1 // indirect - cloud.google.com/go/compute/metadata v0.2.3 // indirect + cloud.google.com/go/compute/metadata v0.3.0 // indirect cloud.google.com/go/iam v1.1.6 // indirect github.com/DataDog/datadog-agent/pkg/util/log v0.58.0 // indirect github.com/DataDog/datadog-agent/pkg/util/scrubber v0.58.0 // indirect @@ -154,6 +155,7 @@ require ( github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect github.com/chenzhuoyu/iasm v0.9.0 // indirect github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575 // indirect + github.com/cncf/xds/go v0.0.0-20240423153145-555b57ec207b // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dustin/go-humanize v1.0.1 // indirect @@ -162,6 +164,7 @@ require ( github.com/eapache/queue v1.1.0 // indirect github.com/ebitengine/purego v0.6.0-alpha.5 // indirect github.com/elastic/elastic-transport-go/v8 v8.1.0 // indirect + github.com/envoyproxy/protoc-gen-validate v1.0.4 // indirect github.com/fatih/color v1.16.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/gabriel-vasile/mimetype v1.4.2 // indirect @@ -240,6 +243,7 @@ require ( github.com/philhofer/fwd v1.1.3-0.20240612014219-fbbf4953d986 // indirect github.com/pierrec/lz4/v4 v4.1.18 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c // indirect github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect @@ -297,8 +301,8 @@ require ( golang.org/x/tools v0.24.0 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240520151616-dc85e6b867a5 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index b05429f344..8d87232f52 100644 --- a/go.sum +++ b/go.sum @@ -177,6 +177,7 @@ cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1h cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM= cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= cloud.google.com/go/contactcenterinsights v1.3.0/go.mod h1:Eu2oemoePuEFc/xKFPjbTuPSj0fYJcPls9TFlPNnHHY= cloud.google.com/go/contactcenterinsights v1.4.0/go.mod h1:L2YzkGbPsv+vMQMCADxJoT9YiTTnSEd6fEvCeHTYVck= cloud.google.com/go/contactcenterinsights v1.6.0/go.mod h1:IIDlT6CLcDoyv79kDv8iWxMSTZhLxSCofVV5W6YFM/w= @@ -890,6 +891,8 @@ github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20240423153145-555b57ec207b h1:ga8SEFjZ60pxLcmhnThWgvH2wg8376yUJmPhEH4H3kw= +github.com/cncf/xds/go v0.0.0-20240423153145-555b57ec207b/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5/go.mod h1:h6jFvWxBdQXxjopDMZyH2UVceIRfR84bdzbkoKrsWNo= github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoCr5oaCLELYA= @@ -1118,9 +1121,13 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.m github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= github.com/envoyproxy/go-control-plane v0.10.3/go.mod h1:fJJn/j26vwOu972OllsvAgJJM//w9BV6Fxbg2LuVd34= +github.com/envoyproxy/go-control-plane v0.13.0 h1:HzkeUz1Knt+3bK+8LG1bxOO/jzWZmdxpwC51i202les= +github.com/envoyproxy/go-control-plane v0.13.0/go.mod h1:GRaKG3dwvFoTg4nj7aXdZnvMg4d7nvT/wl9WgVXn3Q8= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo= github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w= +github.com/envoyproxy/protoc-gen-validate v1.0.4 h1:gVPz/FMfvh57HdSJQyvBtF00j8JU4zdyUgIUNhlgg0A= +github.com/envoyproxy/protoc-gen-validate v1.0.4/go.mod h1:qys6tmnRsYrQqIhm2bvKZH4Blx/1gTIZ2UKVY1M+Yew= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= @@ -1883,6 +1890,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -2483,6 +2492,7 @@ golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI= golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8= +golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -3007,8 +3017,10 @@ google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 h1:9+tzLLstTlPTRyJ google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:mqHbVIp48Muh7Ywss/AD6I5kNVKZMmAa/QEW58Gxp2s= google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 h1:RFiFrvy37/mpSpdySBDrUdipW/dHwsRwh3J3+A9VgT4= google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237/go.mod h1:Z5Iiy3jtmioajWHDGFk7CeugTyHtPvMHA4UTmUkyalE= +google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157/go.mod h1:99sLkeliLXfdj2J75X3Ho+rrVCaJze0uwN7zDDkjPVU= google.golang.org/genproto/googleapis/rpc v0.0.0-20240520151616-dc85e6b867a5 h1:Q2RxlXqh1cgzzUgV261vBO2jI5R/3DD1J2pM0nI4NhU= google.golang.org/genproto/googleapis/rpc v0.0.0-20240520151616-dc85e6b867a5/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= @@ -3055,6 +3067,7 @@ google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwS google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= +google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= diff --git a/internal/appsec/emitter/httpsec/http.go b/internal/appsec/emitter/httpsec/http.go index 41ebfa7e23..1884549e60 100644 --- a/internal/appsec/emitter/httpsec/http.go +++ b/internal/appsec/emitter/httpsec/http.go @@ -12,9 +12,12 @@ package httpsec import ( "context" + "strings" + // Blank import needed to use embed for the default blocked response payloads _ "embed" "net/http" + "net/url" "sync/atomic" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace" @@ -197,3 +200,47 @@ func WrapHandler(handler http.Handler, span ddtrace.Span, pathParams map[string] handler.ServeHTTP(tw, tr) }) } + +// MakeHandlerOperationArgs creates the HandlerOperationArgs value. +func MakeHandlerOperationArgs(headers map[string][]string, method string, host string, remoteAddr string, url *url.URL) HandlerOperationArgs { + cookies := filterCookiesFromHeaders(headers) + + args := HandlerOperationArgs{ + Method: method, + RequestURI: url.RequestURI(), + Host: host, + RemoteAddr: remoteAddr, + Headers: headers, + Cookies: cookies, + QueryParams: url.Query(), + PathParams: map[string]string{}, + } + + args.Headers["host"] = []string{host} + return args +} + +// Separate the cookies from the headers, return the parsed cookies and remove in place the cookies from the headers. +// Headers used for `server.request.headers.no_cookies` and `server.response.headers.no_cookies` addresses for the WAF +// Cookies are used for the `server.request.cookies` address +func filterCookiesFromHeaders(headers http.Header) map[string][]string { + cookieHeader, ok := headers["Cookie"] + if !ok { + return make(http.Header) + } + + delete(headers, "Cookie") + + cookies := make(map[string][]string, len(cookieHeader)) + for _, c := range cookieHeader { + parts := strings.Split(c, ";") + for _, part := range parts { + cookie := strings.Split(part, "=") + if len(cookie) == 2 { + cookies[cookie[0]] = append(cookies[cookie[0]], cookie[1]) + } + } + } + + return cookies +} diff --git a/internal/appsec/emitter/waf/actions/block.go b/internal/appsec/emitter/waf/actions/block.go index ae802b60bd..40418f4410 100644 --- a/internal/appsec/emitter/waf/actions/block.go +++ b/internal/appsec/emitter/waf/actions/block.go @@ -69,6 +69,11 @@ type ( // BlockHTTP are actions that interact with an HTTP request flow BlockHTTP struct { http.Handler + + StatusCode int `mapstructure:"status_code"` + RedirectLocation string + // BlockingTemplate is a function that returns the headers to be added and body to be written to the response + BlockingTemplate func(headers map[string][]string) (map[string][]string, []byte) } ) @@ -125,7 +130,11 @@ func NewBlockAction(params map[string]any) []Action { } func newHTTPBlockRequestAction(status int, template string) *BlockHTTP { - return &BlockHTTP{Handler: newBlockHandler(status, template)} + return &BlockHTTP{ + Handler: newBlockHandler(status, template), + BlockingTemplate: newManualBlockHandler(template), + StatusCode: status, + } } // newBlockHandler creates, initializes and returns a new BlockRequestAction @@ -139,19 +148,50 @@ func newBlockHandler(status int, template string) http.Handler { return htmlHandler default: return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - h := jsonHandler - hdr := r.Header.Get("Accept") - htmlIdx := strings.Index(hdr, "text/html") - jsonIdx := strings.Index(hdr, "application/json") - // Switch to html handler if text/html comes before application/json in the Accept header - if htmlIdx != -1 && (jsonIdx == -1 || htmlIdx < jsonIdx) { - h = htmlHandler - } - h.ServeHTTP(w, r) + h := findCorrectTemplate(jsonHandler, htmlHandler, r.Header.Get("Accept")) + h.(http.Handler).ServeHTTP(w, r) }) } } +func newManualBlockHandler(template string) func(headers map[string][]string) (map[string][]string, []byte) { + htmlHandler := newManualBlockDataHandler("text/html", blockedTemplateHTML) + jsonHandler := newManualBlockDataHandler("application/json", blockedTemplateJSON) + switch template { + case "json": + return jsonHandler + case "html": + return htmlHandler + default: + return func(headers map[string][]string) (map[string][]string, []byte) { + acceptHeader := "" + if hdr, ok := headers["Accept"]; ok && len(hdr) > 0 { + acceptHeader = hdr[0] + } + h := findCorrectTemplate(jsonHandler, htmlHandler, acceptHeader) + return h.(func(headers map[string][]string) (map[string][]string, []byte))(headers) + } + } +} + +func findCorrectTemplate(jsonHandler interface{}, htmlHandler interface{}, acceptHeader string) interface{} { + h := jsonHandler + hdr := acceptHeader + htmlIdx := strings.Index(hdr, "text/html") + jsonIdx := strings.Index(hdr, "application/json") + // Switch to html handler if text/html comes before application/json in the Accept header + if htmlIdx != -1 && (jsonIdx == -1 || htmlIdx < jsonIdx) { + h = htmlHandler + } + return h +} + +func newManualBlockDataHandler(ct string, template []byte) func(headers map[string][]string) (map[string][]string, []byte) { + return func(headers map[string][]string) (map[string][]string, []byte) { + return map[string][]string{"Content-Type": {ct}}, template + } +} + func newBlockRequestHandler(status int, ct string, payload []byte) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", ct) diff --git a/internal/appsec/emitter/waf/actions/http_redirect.go b/internal/appsec/emitter/waf/actions/http_redirect.go index 3cdca4c818..25e209d790 100644 --- a/internal/appsec/emitter/waf/actions/http_redirect.go +++ b/internal/appsec/emitter/waf/actions/http_redirect.go @@ -7,10 +7,14 @@ package actions import ( "net/http" + "path" + "strings" "github.com/mitchellh/mapstructure" "gopkg.in/DataDog/dd-trace-go.v1/internal/log" + + urlpkg "net/url" ) // redirectActionParams are the dynamic parameters to be provided to a "redirect_request" @@ -38,9 +42,17 @@ func newRedirectRequestAction(status int, loc string) *BlockHTTP { // If location is not set we fall back on a default block action if loc == "" { - return &BlockHTTP{Handler: newBlockHandler(http.StatusForbidden, string(blockedTemplateJSON))} + return &BlockHTTP{ + Handler: newBlockHandler(http.StatusForbidden, string(blockedTemplateJSON)), + StatusCode: status, + BlockingTemplate: newManualBlockHandler("json"), + } + } + return &BlockHTTP{ + Handler: http.RedirectHandler(loc, status), + StatusCode: status, + RedirectLocation: loc, } - return &BlockHTTP{Handler: http.RedirectHandler(loc, status)} } // NewRedirectAction creates an action for the "redirect_request" action type @@ -52,3 +64,75 @@ func NewRedirectAction(params map[string]any) []Action { } return []Action{newRedirectRequestAction(p.StatusCode, p.Location)} } + +// HandleRedirectLocationString returns the headers and body to be written to the response when a redirect is needed +// Vendored from net/http/server.go +func HandleRedirectLocationString(oldpath string, url string, statusCode int, method string, h map[string][]string) (map[string][]string, []byte) { + if u, err := urlpkg.Parse(url); err == nil { + // If url was relative, make its path absolute by + // combining with request path. + // The client would probably do this for us, + // but doing it ourselves is more reliable. + // See RFC 7231, section 7.1.2 + if u.Scheme == "" && u.Host == "" { + if oldpath == "" { // should not happen, but avoid a crash if it does + oldpath = "/" + } + + // no leading http://server + if url == "" || url[0] != '/' { + // make relative path absolute + olddir, _ := path.Split(oldpath) + url = olddir + url + } + + var query string + if i := strings.Index(url, "?"); i != -1 { + url, query = url[:i], url[i:] + } + + // clean up but preserve trailing slash + trailing := strings.HasSuffix(url, "/") + url = path.Clean(url) + if trailing && !strings.HasSuffix(url, "/") { + url += "/" + } + url += query + } + } + + // RFC 7231 notes that a short HTML body is usually included in + // the response because older user agents may not understand 301/307. + // Do it only if the request didn't already have a Content-Type header. + _, hadCT := h["content-type"] + newHeaders := make(map[string][]string, 2) + + newHeaders["location"] = []string{url} + if !hadCT && (method == "GET" || method == "HEAD") { + newHeaders["content-length"] = []string{"text/html; charset=utf-8"} + } + + // Shouldn't send the body for POST or HEAD; that leaves GET. + var body []byte + if !hadCT && method == "GET" { + body = []byte("" + http.StatusText(statusCode) + ".\n") + } + + return newHeaders, body +} + +// Vendored from net/http/server.go +var htmlReplacer = strings.NewReplacer( + "&", "&", + "<", "<", + ">", ">", + // """ is shorter than """. + `"`, """, + // "'" is shorter than "'" and apos was not in HTML until HTML5. + "'", "'", +) + +// htmlEscape escapes special characters like "<" to become "<". +func htmlEscape(s string) string { + return htmlReplacer.Replace(s) +} diff --git a/internal/appsec/listener/httpsec/http.go b/internal/appsec/listener/httpsec/http.go index 08b9e853dd..804d115fcd 100644 --- a/internal/appsec/listener/httpsec/http.go +++ b/internal/appsec/listener/httpsec/http.go @@ -59,7 +59,7 @@ func (feature *Feature) OnRequest(op *httpsec.HandlerOperation, args httpsec.Han headers := headersRemoveCookies(args.Headers) headers["host"] = []string{args.Host} - setRequestHeadersTags(op, headers) + SetRequestHeadersTags(op, headers) op.Run(op, addresses.NewAddressesBuilder(). @@ -76,7 +76,7 @@ func (feature *Feature) OnRequest(op *httpsec.HandlerOperation, args httpsec.Han func (feature *Feature) OnResponse(op *httpsec.HandlerOperation, resp httpsec.HandlerOperationRes) { headers := headersRemoveCookies(resp.Headers) - setResponseHeadersTags(op, headers) + SetResponseHeadersTags(op, headers) builder := addresses.NewAddressesBuilder(). WithResponseHeadersNoCookies(headers). diff --git a/internal/appsec/listener/httpsec/request.go b/internal/appsec/listener/httpsec/request.go index abd3983183..cb181be81e 100644 --- a/internal/appsec/listener/httpsec/request.go +++ b/internal/appsec/listener/httpsec/request.go @@ -149,13 +149,13 @@ func readMonitoredClientIPHeadersConfig() { } } -// setRequestHeadersTags sets the AppSec-specific request headers span tags. -func setRequestHeadersTags(span trace.TagSetter, headers map[string][]string) { +// SetRequestHeadersTags sets the AppSec-specific request headers span tags. +func SetRequestHeadersTags(span trace.TagSetter, headers map[string][]string) { setHeadersTags(span, "http.request.headers.", headers) } -// setResponseHeadersTags sets the AppSec-specific response headers span tags. -func setResponseHeadersTags(span trace.TagSetter, headers map[string][]string) { +// SetResponseHeadersTags sets the AppSec-specific response headers span tags. +func SetResponseHeadersTags(span trace.TagSetter, headers map[string][]string) { setHeadersTags(span, "http.response.headers.", headers) } diff --git a/internal/appsec/listener/httpsec/request_test.go b/internal/appsec/listener/httpsec/request_test.go index 38052cbb96..badac1891c 100644 --- a/internal/appsec/listener/httpsec/request_test.go +++ b/internal/appsec/listener/httpsec/request_test.go @@ -215,8 +215,8 @@ func TestTags(t *testing.T) { return } require.NoError(t, err) - setRequestHeadersTags(&span, reqHeadersCase.headers) - setResponseHeadersTags(&span, respHeadersCase.headers) + SetRequestHeadersTags(&span, reqHeadersCase.headers) + SetResponseHeadersTags(&span, respHeadersCase.headers) if eventCase.events != nil { require.Subset(t, span.Tags, map[string]interface{}{ diff --git a/internal/appsec/listener/trace/trace.go b/internal/appsec/listener/trace/trace.go index 45fb28e99f..709ed31ecf 100644 --- a/internal/appsec/listener/trace/trace.go +++ b/internal/appsec/listener/trace/trace.go @@ -6,6 +6,7 @@ package trace import ( + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace" "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/config" "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/dyngo" "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/emitter/trace" @@ -19,6 +20,12 @@ var staticAppsecTags = map[string]any{ "_dd.runtime_family": "go", } +func SetAppsecStaticTags(span ddtrace.Span) { + for key, value := range staticAppsecTags { + span.SetTag(key, value) + } +} + type AppsecSpanTransport struct{} func (*AppsecSpanTransport) String() string { diff --git a/internal/appsec/testdata/user_rules.json b/internal/appsec/testdata/user_rules.json index 6acb14089e..a13a0d67ed 100644 --- a/internal/appsec/testdata/user_rules.json +++ b/internal/appsec/testdata/user_rules.json @@ -53,6 +53,33 @@ "block" ] }, + { + "id": "tst-037-008", + "name": "Test block on cookies", + "tags": { + "type": "lfi", + "crs_id": "000008", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.cookies" + } + ], + "regex": "jdfoSDGFkivRG_234" + }, + "operator": "match_regex" + } + ], + "transformers": [], + "on_match": [ + "block" + ] + }, + { "id": "headers-003", "name": "query match", From a2f01c2926be10db864ae3152ff8b95622b67dda Mon Sep 17 00:00:00 2001 From: Flavien Darche Date: Tue, 5 Nov 2024 21:24:53 +0100 Subject: [PATCH 02/14] Remove binary to move it to another branch --- .../workflows/service-extensions-publish.yml | 99 -------------- .../envoy/cmd/serviceextensions/.gitignore | 1 - .../envoy/cmd/serviceextensions/Dockerfile | 20 --- .../envoy/cmd/serviceextensions/localhost.crt | 19 --- .../envoy/cmd/serviceextensions/localhost.key | 27 ---- .../envoy/cmd/serviceextensions/main.go | 128 ------------------ 6 files changed, 294 deletions(-) delete mode 100644 .github/workflows/service-extensions-publish.yml delete mode 100644 contrib/envoyproxy/envoy/cmd/serviceextensions/.gitignore delete mode 100644 contrib/envoyproxy/envoy/cmd/serviceextensions/Dockerfile delete mode 100644 contrib/envoyproxy/envoy/cmd/serviceextensions/localhost.crt delete mode 100644 contrib/envoyproxy/envoy/cmd/serviceextensions/localhost.key delete mode 100644 contrib/envoyproxy/envoy/cmd/serviceextensions/main.go diff --git a/.github/workflows/service-extensions-publish.yml b/.github/workflows/service-extensions-publish.yml deleted file mode 100644 index 9e0c645be0..0000000000 --- a/.github/workflows/service-extensions-publish.yml +++ /dev/null @@ -1,99 +0,0 @@ -name: Publish Service Extensions Callout images packages - -on: - push: - branches: - - 'flavien/service-extensions' - release: - types: - - published - workflow_dispatch: - inputs: - tag_name: - description: 'Docker image tag to use for the package' - required: true - default: 'dev' - commit_sha: - description: 'Commit SHA to checkout' - required: true - -permissions: - contents: read - packages: write - -jobs: - publish-service-extensions: - runs-on: ubuntu-latest - steps: - - - name: Get tag name - id: get_tag_name - run: | - if [ "${{ github.event_name }}" = "release" ]; then - echo "::set-output name=tag::${{ github.event.release.tag_name }}" - echo "Here1: tag=${{ github.event.release.tag_name }}" - else - if [ -z "${{ github.event.inputs.tag_name }}" ]; then - echo "::set-output name=tag::dev" - echo "Here2: tag=dev" - else - echo "::set-output name=tag::${{ github.event.inputs.tag_name }}" - echo "Here3: tag=${{ github.event.inputs.tag_name }}" - fi - fi - echo "Finally: ${{ steps.get_tag_name.outputs.tag }}" - - - name: Checkout - uses: actions/checkout@v4 - if: github.event_name == 'release' - with: - ref: ${{ steps.get_tag_name.outputs.tag }} - - - name: Checkout - uses: actions/checkout@v4 - if: github.event_name != 'release' - with: - ref: ${{ github.event.inputs.commit_sha || github.sha }} - - - name: Set up Go 1.22 - uses: actions/setup-go@v5 - with: - go-version: 1.22 - id: go - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to Docker - shell: bash - run: docker login -u publisher -p ${{ secrets.GITHUB_TOKEN }} ghcr.io - - - name: Build and push [dev] - id: build-dev - if: github.event_name != 'release' - uses: docker/build-push-action@v6 - with: - context: . - file: ./contrib/envoyproxy/envoy/cmd/serviceextensions/Dockerfile - platforms: linux/amd64,linux/arm64 - push: true - tags: | # Use the commit SHA from the manual trigger or default to the SHA from the push event - ghcr.io/datadog/dd-trace-go/service-extensions-callout:${{ steps.get_tag_name.outputs.tag }} - ghcr.io/datadog/dd-trace-go/service-extensions-callout:${{ github.event.inputs.commit_sha || github.sha }} - - - name: Build and push [release] - id: build-release - if: github.event_name == 'release' - uses: docker/build-push-action@v6 - with: - context: . - file: ./contrib/envoyproxy/envoy/cmd/serviceextensions/Dockerfile - platforms: linux/amd64,linux/arm64 - push: true - tags: | - ghcr.io/datadog/dd-trace-go/service-extensions-callout:latest - ghcr.io/datadog/dd-trace-go/service-extensions-callout:${{ steps.get_tag_name.outputs.tag }} - ghcr.io/datadog/dd-trace-go/service-extensions-callout:${{ github.sha }} \ No newline at end of file diff --git a/contrib/envoyproxy/envoy/cmd/serviceextensions/.gitignore b/contrib/envoyproxy/envoy/cmd/serviceextensions/.gitignore deleted file mode 100644 index 68295c4a55..0000000000 --- a/contrib/envoyproxy/envoy/cmd/serviceextensions/.gitignore +++ /dev/null @@ -1 +0,0 @@ -serviceextensions \ No newline at end of file diff --git a/contrib/envoyproxy/envoy/cmd/serviceextensions/Dockerfile b/contrib/envoyproxy/envoy/cmd/serviceextensions/Dockerfile deleted file mode 100644 index 87136d2cba..0000000000 --- a/contrib/envoyproxy/envoy/cmd/serviceextensions/Dockerfile +++ /dev/null @@ -1,20 +0,0 @@ -# Build stage -FROM golang:1.22-alpine AS builder -ENV CGO_ENABLED=1 -WORKDIR /app -COPY . . -RUN apk add --no-cache --update git build-base -RUN go build -o ./contrib/envoyproxy/envoy/cmd/serviceextensions/serviceextensions ./contrib/envoyproxy/envoy/cmd/serviceextensions - -# Runtime stage -FROM alpine:3.20.3 -RUN apk --no-cache add ca-certificates tzdata libc6-compat libgcc libstdc++ -WORKDIR /app -COPY --from=builder /app/contrib/envoyproxy/envoy/cmd/serviceextensions/serviceextensions /app/serviceextensions -COPY ./contrib/envoyproxy/envoy/cmd/serviceextensions/localhost.crt /app/localhost.crt -COPY ./contrib/envoyproxy/envoy/cmd/serviceextensions/localhost.key /app/localhost.key - -EXPOSE 80 -EXPOSE 443 - -CMD ["./serviceextensions"] diff --git a/contrib/envoyproxy/envoy/cmd/serviceextensions/localhost.crt b/contrib/envoyproxy/envoy/cmd/serviceextensions/localhost.crt deleted file mode 100644 index fc54fd492e..0000000000 --- a/contrib/envoyproxy/envoy/cmd/serviceextensions/localhost.crt +++ /dev/null @@ -1,19 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDFjCCAf4CCQCzrLIhrWa55zANBgkqhkiG9w0BAQsFADBCMQswCQYDVQQGEwJV -UzETMBEGA1UECAwKQ2FsaWZvcm5pYTEPMA0GA1UECgwGR29vZ2xlMQ0wCwYDVQQL -DARnUlBDMCAXDTE5MDYyNDIyMjIzM1oYDzIxMTkwNTMxMjIyMjMzWjBWMQswCQYD -VQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEPMA0GA1UECgwGR29vZ2xlMQ0w -CwYDVQQLDARnUlBDMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEB -AQUAA4IBDwAwggEKAoIBAQCtCW0TjugnIUu8BEVIYvdMP+/2GENQDjZhZ8eKR5C6 -toDGbgjsDtt/GxISAg4cg70fIvy0XolnGPZodvfHDM4lJ7yHBOdZD8TXQoE6okR7 -HZuLUJ20M0pXgWqtRewKRUjuYsSDXBnzLiZw1dcv9nGpo+Bqa8NonpiGRRpEkshF -D6T9KU9Ts/x+wMQBIra2Gj0UMh79jPhUuxcYAQA0JQGivnOtdwuPiumpnUT8j8h6 -tWg5l01EsCZWJecCF85KnGpJEVYPyPqBqGsy0nGS9plGotOWF87+jyUQt+KD63xA -aBmTro86mKDDKEK4JvzjVeMGz2UbVcLPiiZnErTFaiXJAgMBAAEwDQYJKoZIhvcN -AQELBQADggEBAKsDgOPCWp5WCy17vJbRlgfgk05sVNIHZtzrmdswjBmvSg8MUpep -XqcPNUpsljAXsf9UM5IFEMRdilUsFGWvHjBEtNAW8WUK9UV18WRuU//0w1Mp5HAN -xUEKb4BoyZr65vlCnTR+AR5c9FfPvLibhr5qHs2RA8Y3GyLOcGqBWed87jhdQLCc -P1bxB+96le5JeXq0tw215lxonI2/3ZYVK4/ok9gwXrQoWm8YieJqitk/ZQ4S17/4 -pynHtDfdxLn23EXeGx+UTxJGfpRmhEZdJ+MN7QGYoomzx5qS5XoYKxRNrDlirJpr -OqXIn8E1it+6d5gOZfuHawcNGhRLplE/pfA= ------END CERTIFICATE----- diff --git a/contrib/envoyproxy/envoy/cmd/serviceextensions/localhost.key b/contrib/envoyproxy/envoy/cmd/serviceextensions/localhost.key deleted file mode 100644 index 72e2463282..0000000000 --- a/contrib/envoyproxy/envoy/cmd/serviceextensions/localhost.key +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEogIBAAKCAQEArQltE47oJyFLvARFSGL3TD/v9hhDUA42YWfHikeQuraAxm4I -7A7bfxsSEgIOHIO9HyL8tF6JZxj2aHb3xwzOJSe8hwTnWQ/E10KBOqJEex2bi1Cd -tDNKV4FqrUXsCkVI7mLEg1wZ8y4mcNXXL/ZxqaPgamvDaJ6YhkUaRJLIRQ+k/SlP -U7P8fsDEASK2tho9FDIe/Yz4VLsXGAEANCUBor5zrXcLj4rpqZ1E/I/IerVoOZdN -RLAmViXnAhfOSpxqSRFWD8j6gahrMtJxkvaZRqLTlhfO/o8lELfig+t8QGgZk66P -OpigwyhCuCb841XjBs9lG1XCz4omZxK0xWolyQIDAQABAoIBADeq/Kh6JT3RfGf0 -h8WN8TlaqHxnueAbcmtL0+oss+cdp7gu1jf7X6o4r0uT1a5ew40s2Fe+wj2kzkE1 -ZOlouTlC22gkr7j7Vbxa7PBMG/Pvxoa/XL0IczZLsGImSJXVTG1E4SvRiZeulTdf -1GbdxhtpWV1jZe5Wd4Na3+SHxF5S7m3PrHiZlYdz1ND+8XZs1NlL9+ej72qSFul9 -t/QjMWJ9pky/Wad5abnRLRyOsg+BsgnXbkUy2rD89ZxFMLda9pzXo3TPyAlBHonr -mkEsE4eRMWMpjBM79JbeyDdHn/cs/LjAZrzeDf7ugXr2CHQpKaM5O0PsNHezJII9 -L5kCfzECgYEA4M/rz1UP1/BJoSqigUlSs0tPAg8a5UlkVsh6Osuq72IPNo8qg/Fw -oV/IiIS+q+obRcFj1Od3PGdTpCJwW5dzd2fXBQGmGdj0HucnCrs13RtBh91JiF5i -y/YYI9KfgOG2ZT9gG68T0gTs6jRrS3Qd83npqjrkJqMOd7s00MK9tUcCgYEAxQq7 -T541oCYHSBRIIb0IrR25krZy9caxzCqPDwOcuuhaCqCiaq+ATvOWlSfgecm4eH0K -PCH0xlWxG0auPEwm4pA8+/WR/XJwscPZMuoht1EoKy1his4eKx/s7hHNeO6KOF0V -Y/zqIiuZnEwUoKbn7EqqNFSTT65PJKyGsICJFG8CgYAfaw9yl1myfQNdQb8aQGwN -YJ33FLNWje427qeeZe5KrDKiFloDvI9YDjHRWnPnRL1w/zj7fSm9yFb5HlMDieP6 -MQnsyjEzdY2QcA+VwVoiv3dmDHgFVeOKy6bOAtaFxYWfGr9MvygO9t9BT/gawGyb -JVORlc9i0vDnrMMR1dV7awKBgBpTWLtGc/u1mPt0Wj7HtsUKV6TWY32a0l5owTxM -S0BdksogtBJ06DukJ9Y9wawD23WdnyRxlPZ6tHLkeprrwbY7dypioOKvy4a0l+xJ -g7+uRCOgqIuXBkjUtx8HmeAyXp0xMo5tWArAsIFFWOwt4IadYygitJvMuh44PraO -NcJZAoGADEiV0dheXUCVr8DrtSom8DQMj92/G/FIYjXL8OUhh0+F+YlYP0+F8PEU -yYIWEqL/S5tVKYshimUXQa537JcRKsTVJBG/ZKD2kuqgOc72zQy3oplimXeJDCXY -h2eAQ0u8GN6tN9C4t8Kp4a3y6FGsxgu+UTxdnL3YQ+yHAVhtCzo= ------END RSA PRIVATE KEY----- diff --git a/contrib/envoyproxy/envoy/cmd/serviceextensions/main.go b/contrib/envoyproxy/envoy/cmd/serviceextensions/main.go deleted file mode 100644 index fcd86b8fb3..0000000000 --- a/contrib/envoyproxy/envoy/cmd/serviceextensions/main.go +++ /dev/null @@ -1,128 +0,0 @@ -package main - -import ( - "crypto/tls" - "gopkg.in/DataDog/dd-trace-go.v1/contrib/envoyproxy/envoy" - "gopkg.in/DataDog/dd-trace-go.v1/internal/log" - "gopkg.in/DataDog/dd-trace-go.v1/internal/version" - "net" - "net/http" - "os" - - extproc "github.com/envoyproxy/go-control-plane/envoy/service/ext_proc/v3" - "github.com/gorilla/mux" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials" - "google.golang.org/grpc/reflection" - "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" -) - -// AppsecCalloutExtensionService defines the struct that follows the ExternalProcessorServer interface. -type AppsecCalloutExtensionService struct { - extproc.ExternalProcessorServer -} - -type serviceExtensionConfig struct { - extensionPort string - extensionHost string - healthcheckPort string -} - -func loadConfig() serviceExtensionConfig { - extensionPort := os.Getenv("DD_SERVICE_EXTENSION_PORT") - if extensionPort == "" { - extensionPort = "443" - } - - extensionHost := os.Getenv("DD_SERVICE_EXTENSION_HOST") - if extensionHost == "" { - extensionHost = "0.0.0.0" - } - - healthcheckPort := os.Getenv("DD_SERVICE_EXTENSION_HEALTHCHECK_PORT") - if healthcheckPort == "" { - healthcheckPort = "80" - } - - return serviceExtensionConfig{ - extensionPort: extensionPort, - extensionHost: extensionHost, - healthcheckPort: healthcheckPort, - } -} - -func main() { - var extensionService AppsecCalloutExtensionService - - // Force set ASM as enabled only if the environment variable is not set - // Note: If the environment variable is set to false, it should be disabled - if os.Getenv("DD_APPSEC_ENABLED") == "" { - if err := os.Setenv("DD_APPSEC_ENABLED", "1"); err != nil { - log.Error("service_extension: failed to set DD_APPSEC_ENABLED environment variable: %v\n", err) - } - } - - // TODO: Enable ASM standalone mode when it is developed (should be done for Q4 2024) - - // Set the DD_VERSION to the current tracer version if not set - if os.Getenv("DD_VERSION") == "" { - if err := os.Setenv("DD_VERSION", version.Tag); err != nil { - log.Error("service_extension: failed to set DD_VERSION environment variable: %v\n", err) - } - } - - config := loadConfig() - - tracer.Start() - - go StartGPRCSsl(&extensionService, config) - log.Info("service_extension: callout gRPC server started on %s:%s\n", config.extensionHost, config.extensionPort) - - go startHealthCheck(config) - log.Info("service_extension: health check server started on %s:%s\n", config.extensionHost, config.healthcheckPort) - - select {} -} - -func startHealthCheck(config serviceExtensionConfig) { - muxServer := mux.NewRouter() - muxServer.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - _, err := w.Write([]byte(`{"status": "ok", "library": {"language": "golang", "version": "` + version.Tag + `"}}`)) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - return - } - - w.WriteHeader(http.StatusOK) - }) - - server := &http.Server{ - Addr: config.extensionHost + ":" + config.healthcheckPort, - Handler: muxServer, - } - - println(server.ListenAndServe()) -} - -func StartGPRCSsl(service extproc.ExternalProcessorServer, config serviceExtensionConfig) { - cert, err := tls.LoadX509KeyPair("localhost.crt", "localhost.key") - if err != nil { - log.Error("Failed to load key pair: %v\n", err) - } - - lis, err := net.Listen("tcp", config.extensionHost+":"+config.extensionPort) - if err != nil { - log.Error("Failed to listen: %v\n", err) - } - - si := envoy.StreamServerInterceptor() - creds := credentials.NewServerTLSFromCert(&cert) - grpcServer := grpc.NewServer(grpc.StreamInterceptor(si), grpc.Creds(creds)) - - extproc.RegisterExternalProcessorServer(grpcServer, service) - reflection.Register(grpcServer) - if err := grpcServer.Serve(lis); err != nil { - log.Error("service_extension: failed to serve gRPC: %v\n", err) - } -} From f3bcc4d41e1fd4c205f18e58db32c664760a3e3d Mon Sep 17 00:00:00 2001 From: Flavien Darche Date: Tue, 5 Nov 2024 21:44:22 +0100 Subject: [PATCH 03/14] Fix CI --- contrib/envoyproxy/envoy/envoy.go | 15 ++++++++++++++- internal/apps/go.mod | 2 +- internal/apps/go.sum | 4 ++-- internal/exectracetest/go.mod | 2 +- internal/exectracetest/go.sum | 4 ++-- 5 files changed, 20 insertions(+), 7 deletions(-) diff --git a/contrib/envoyproxy/envoy/envoy.go b/contrib/envoyproxy/envoy/envoy.go index 2002d171e3..413d475726 100644 --- a/contrib/envoyproxy/envoy/envoy.go +++ b/contrib/envoyproxy/envoy/envoy.go @@ -8,7 +8,9 @@ package envoy import ( "context" "errors" + "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry" "io" + "math" "net/http" "net/url" "strconv" @@ -37,6 +39,13 @@ import ( "gopkg.in/DataDog/dd-trace-go.v1/internal/log" ) +const componentName = "envoy/service/ext_proc/v3" + +func init() { + telemetry.LoadIntegration(componentName) + tracer.MarkIntegrationImported("github.com/envoyproxy/go-control-plane/envoy/service/ext_proc/v3") +} + type CurrentRequest struct { op *httpsec.HandlerOperation blockAction *atomic.Pointer[actions.BlockHTTP] @@ -379,12 +388,16 @@ func doBlockRequest(currentRequest *CurrentRequest, blockAction *actions.BlockHT httpsec2.SetResponseHeadersTags(currentRequest.span, headerToSet) currentRequest.statusCode = blockAction.StatusCode + var int32StatusCode int32 = 0 + if currentRequest.statusCode > 0 && currentRequest.statusCode <= math.MaxInt32 { + int32StatusCode = int32(currentRequest.statusCode) + } return &extproc.ProcessingResponse{ Response: &extproc.ProcessingResponse_ImmediateResponse{ ImmediateResponse: &extproc.ImmediateResponse{ Status: &v32.HttpStatus{ - Code: v32.StatusCode(currentRequest.statusCode), + Code: v32.StatusCode(int32StatusCode), }, Headers: &extproc.HeaderMutation{ SetHeaders: headersMutation, diff --git a/internal/apps/go.mod b/internal/apps/go.mod index 9ce5485a85..4a3917bb42 100644 --- a/internal/apps/go.mod +++ b/internal/apps/go.mod @@ -56,7 +56,7 @@ require ( golang.org/x/text v0.17.0 // indirect golang.org/x/tools v0.24.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240730163845-b1a4ccb954bf // indirect - google.golang.org/grpc v1.64.1 // indirect + google.golang.org/grpc v1.65.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/internal/apps/go.sum b/internal/apps/go.sum index a591990f7b..4aec2858b9 100644 --- a/internal/apps/go.sum +++ b/internal/apps/go.sum @@ -282,8 +282,8 @@ google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAs google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto/googleapis/rpc v0.0.0-20240730163845-b1a4ccb954bf h1:liao9UHurZLtiEwBgT9LMOnKYsHze6eA6w1KQCMVN2Q= google.golang.org/genproto/googleapis/rpc v0.0.0-20240730163845-b1a4ccb954bf/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= -google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA= -google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0= +google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= +google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= diff --git a/internal/exectracetest/go.mod b/internal/exectracetest/go.mod index 0bc1db3393..2efa62f766 100644 --- a/internal/exectracetest/go.mod +++ b/internal/exectracetest/go.mod @@ -73,7 +73,7 @@ require ( golang.org/x/tools v0.24.0 // indirect golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240730163845-b1a4ccb954bf // indirect - google.golang.org/grpc v1.64.1 // indirect + google.golang.org/grpc v1.65.0 // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/internal/exectracetest/go.sum b/internal/exectracetest/go.sum index 0d8bd345c4..0e710320f7 100644 --- a/internal/exectracetest/go.sum +++ b/internal/exectracetest/go.sum @@ -290,8 +290,8 @@ google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAs google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto/googleapis/rpc v0.0.0-20240730163845-b1a4ccb954bf h1:liao9UHurZLtiEwBgT9LMOnKYsHze6eA6w1KQCMVN2Q= google.golang.org/genproto/googleapis/rpc v0.0.0-20240730163845-b1a4ccb954bf/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= -google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA= -google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0= +google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= +google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From e3a3dbcfe1a0fceb359196b0fff467745dbe753a Mon Sep 17 00:00:00 2001 From: Flavien Darche Date: Fri, 8 Nov 2024 15:23:02 +0100 Subject: [PATCH 04/14] Apply comments --- contrib/envoyproxy/envoy/envoy.go | 37 ++++++++----------- contrib/envoyproxy/envoy/envoy_test.go | 1 - internal/appsec/emitter/httpsec/http.go | 1 - internal/appsec/listener/httpsec/http.go | 4 +- internal/appsec/listener/httpsec/request.go | 8 ++-- .../appsec/listener/httpsec/request_test.go | 4 +- internal/appsec/listener/trace/trace.go | 7 ---- 7 files changed, 23 insertions(+), 39 deletions(-) diff --git a/contrib/envoyproxy/envoy/envoy.go b/contrib/envoyproxy/envoy/envoy.go index 413d475726..ca407bba43 100644 --- a/contrib/envoyproxy/envoy/envoy.go +++ b/contrib/envoyproxy/envoy/envoy.go @@ -17,6 +17,10 @@ import ( "strings" "sync/atomic" + corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" + v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" + extproc "github.com/envoyproxy/go-control-plane/envoy/service/ext_proc/v3" + v32 "github.com/envoyproxy/go-control-plane/envoy/type/v3" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/metadata" @@ -25,12 +29,6 @@ import ( "gopkg.in/DataDog/dd-trace-go.v1/ddtrace" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext" "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/emitter/waf/actions" - "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/listener/trace" - - corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" - v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" - extproc "github.com/envoyproxy/go-control-plane/envoy/service/ext_proc/v3" - v32 "github.com/envoyproxy/go-control-plane/envoy/type/v3" "gopkg.in/DataDog/dd-trace-go.v1/contrib/internal/httptrace" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" @@ -87,9 +85,7 @@ func StreamServerInterceptor(opts ...grpctrace.Option) grpc.StreamServerIntercep } // Close the span when the request is done processing - defer func() { - closeSpan(currentRequest) - }() + defer closeSpan(currentRequest) for { select { @@ -104,8 +100,7 @@ func StreamServerInterceptor(opts ...grpctrace.Option) grpc.StreamServerIntercep } var req extproc.ProcessingRequest - err := ss.RecvMsg(&req) - if err != nil { + if err := ss.RecvMsg(&req); 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 { @@ -222,7 +217,7 @@ func ProcessRequestHeaders(ctx context.Context, req *extproc.ProcessingRequest_R currentRequest.op, currentRequest.blockAction, _ = httpsec.StartOperation(ctx, currentRequest.requestArgs) // Block handling: If triggered, we need to block the request, return an immediate response - if blockPtr := currentRequest.blockAction.Load(); blockPtr != nil { + if blockPtr := currentRequest.blockAction.Swap(nil); blockPtr != nil { response := doBlockRequest(currentRequest, blockPtr, headers) return response, nil } @@ -244,7 +239,7 @@ func verifyRequestHttp2RequestHeaders(headers map[string][]string) (string, stri // :authority, :scheme, :path, :method for _, header := range []string{":authority", ":scheme", ":path", ":method"} { - if _, ok := headers[header]; !ok { + if _, ok := headers[header]; !ok || len(headers[header]) == 0 { return "", "", "", "", status.Errorf(codes.InvalidArgument, "Missing required header: %v", header) } } @@ -255,7 +250,7 @@ func verifyRequestHttp2RequestHeaders(headers map[string][]string) (string, stri func verifyRequestHttp2ResponseHeaders(headers map[string][]string) (string, error) { // :status - if _, ok := headers[":status"]; !ok { + if _, ok := headers[":status"]; !ok || len(headers[":status"]) == 0 { return "", status.Errorf(codes.InvalidArgument, "Missing required header: %v", ":status") } @@ -286,12 +281,10 @@ func ProcessResponseHeaders(res *extproc.ProcessingRequest_ResponseHeaders, curr currentRequest.op = nil // Block handling: If triggered, we need to block the request, return an immediate response - if blockPtr := currentRequest.blockAction.Load(); blockPtr != nil { + if blockPtr := currentRequest.blockAction.Swap(nil); blockPtr != nil { return doBlockRequest(currentRequest, blockPtr, headers), nil } - httpsec2.SetResponseHeadersTags(currentRequest.span, headers) - // Note: (cf. comment in the stream error handling) // The end of stream bool value is not reliable if res.ResponseHeaders.GetEndOfStream() { @@ -311,7 +304,7 @@ func ProcessResponseHeaders(res *extproc.ProcessingRequest_ResponseHeaders, curr func createExternalProcessedSpan(ctx context.Context, headers map[string][]string, method string, host string, path string, remoteAddr string, ipTags map[string]string, parsedUrl *url.URL) tracer.Span { userAgent := "" - if ua, ok := headers["User-Agent"]; ok { + if ua, ok := headers["User-Agent"]; ok || len(ua) > 0 { userAgent = ua[0] } @@ -336,9 +329,6 @@ func createExternalProcessedSpan(ctx context.Context, headers map[string][]strin }..., ) - httpsec2.SetRequestHeadersTags(span, headers) - trace.SetAppsecStaticTags(span) - return span } @@ -350,6 +340,10 @@ func separateEnvoyHeaders(receivedHeaders []*corev3.HeaderValue) (map[string][]s pseudoHeadersHttp2 := make(map[string][]string) for _, v := range receivedHeaders { key := v.GetKey() + if len(key) == 0 { + continue + } + if key[0] == ':' { pseudoHeadersHttp2[key] = []string{string(v.GetRawValue())} } else { @@ -386,7 +380,6 @@ func doBlockRequest(currentRequest *CurrentRequest, blockAction *actions.BlockHT }) } - httpsec2.SetResponseHeadersTags(currentRequest.span, headerToSet) currentRequest.statusCode = blockAction.StatusCode var int32StatusCode int32 = 0 if currentRequest.statusCode > 0 && currentRequest.statusCode <= math.MaxInt32 { diff --git a/contrib/envoyproxy/envoy/envoy_test.go b/contrib/envoyproxy/envoy/envoy_test.go index e3f872f87f..a1cea81a3a 100644 --- a/contrib/envoyproxy/envoy/envoy_test.go +++ b/contrib/envoyproxy/envoy/envoy_test.go @@ -385,7 +385,6 @@ func TestGeneratedSpan(t *testing.T) { require.Equal(t, "GET", span.Tag("http.method")) require.Equal(t, "datadoghq.com", span.Tag("http.host")) require.Equal(t, "GET /resource-span", span.Tag("resource.name")) - require.Equal(t, "datadoghq.com", span.Tag("http.request.headers.host")) require.Equal(t, "server", span.Tag("span.kind")) require.Equal(t, "Mistake Not...", span.Tag("http.useragent")) }) diff --git a/internal/appsec/emitter/httpsec/http.go b/internal/appsec/emitter/httpsec/http.go index 1884549e60..2602b6ff16 100644 --- a/internal/appsec/emitter/httpsec/http.go +++ b/internal/appsec/emitter/httpsec/http.go @@ -216,7 +216,6 @@ func MakeHandlerOperationArgs(headers map[string][]string, method string, host s PathParams: map[string]string{}, } - args.Headers["host"] = []string{host} return args } diff --git a/internal/appsec/listener/httpsec/http.go b/internal/appsec/listener/httpsec/http.go index 804d115fcd..08b9e853dd 100644 --- a/internal/appsec/listener/httpsec/http.go +++ b/internal/appsec/listener/httpsec/http.go @@ -59,7 +59,7 @@ func (feature *Feature) OnRequest(op *httpsec.HandlerOperation, args httpsec.Han headers := headersRemoveCookies(args.Headers) headers["host"] = []string{args.Host} - SetRequestHeadersTags(op, headers) + setRequestHeadersTags(op, headers) op.Run(op, addresses.NewAddressesBuilder(). @@ -76,7 +76,7 @@ func (feature *Feature) OnRequest(op *httpsec.HandlerOperation, args httpsec.Han func (feature *Feature) OnResponse(op *httpsec.HandlerOperation, resp httpsec.HandlerOperationRes) { headers := headersRemoveCookies(resp.Headers) - SetResponseHeadersTags(op, headers) + setResponseHeadersTags(op, headers) builder := addresses.NewAddressesBuilder(). WithResponseHeadersNoCookies(headers). diff --git a/internal/appsec/listener/httpsec/request.go b/internal/appsec/listener/httpsec/request.go index cb181be81e..abd3983183 100644 --- a/internal/appsec/listener/httpsec/request.go +++ b/internal/appsec/listener/httpsec/request.go @@ -149,13 +149,13 @@ func readMonitoredClientIPHeadersConfig() { } } -// SetRequestHeadersTags sets the AppSec-specific request headers span tags. -func SetRequestHeadersTags(span trace.TagSetter, headers map[string][]string) { +// setRequestHeadersTags sets the AppSec-specific request headers span tags. +func setRequestHeadersTags(span trace.TagSetter, headers map[string][]string) { setHeadersTags(span, "http.request.headers.", headers) } -// SetResponseHeadersTags sets the AppSec-specific response headers span tags. -func SetResponseHeadersTags(span trace.TagSetter, headers map[string][]string) { +// setResponseHeadersTags sets the AppSec-specific response headers span tags. +func setResponseHeadersTags(span trace.TagSetter, headers map[string][]string) { setHeadersTags(span, "http.response.headers.", headers) } diff --git a/internal/appsec/listener/httpsec/request_test.go b/internal/appsec/listener/httpsec/request_test.go index badac1891c..38052cbb96 100644 --- a/internal/appsec/listener/httpsec/request_test.go +++ b/internal/appsec/listener/httpsec/request_test.go @@ -215,8 +215,8 @@ func TestTags(t *testing.T) { return } require.NoError(t, err) - SetRequestHeadersTags(&span, reqHeadersCase.headers) - SetResponseHeadersTags(&span, respHeadersCase.headers) + setRequestHeadersTags(&span, reqHeadersCase.headers) + setResponseHeadersTags(&span, respHeadersCase.headers) if eventCase.events != nil { require.Subset(t, span.Tags, map[string]interface{}{ diff --git a/internal/appsec/listener/trace/trace.go b/internal/appsec/listener/trace/trace.go index 709ed31ecf..45fb28e99f 100644 --- a/internal/appsec/listener/trace/trace.go +++ b/internal/appsec/listener/trace/trace.go @@ -6,7 +6,6 @@ package trace import ( - "gopkg.in/DataDog/dd-trace-go.v1/ddtrace" "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/config" "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/dyngo" "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/emitter/trace" @@ -20,12 +19,6 @@ var staticAppsecTags = map[string]any{ "_dd.runtime_family": "go", } -func SetAppsecStaticTags(span ddtrace.Span) { - for key, value := range staticAppsecTags { - span.SetTag(key, value) - } -} - type AppsecSpanTransport struct{} func (*AppsecSpanTransport) String() string { From 75b62409c93fccf7a68d1bab227922aae38837d9 Mon Sep 17 00:00:00 2001 From: Eliott Bouhana Date: Fri, 8 Nov 2024 18:42:07 +0100 Subject: [PATCH 05/14] revert all changes expect in contrib/envoyproxy Signed-off-by: Eliott Bouhana --- contrib/internal/httptrace/httptrace.go | 78 ++++------------ go.mod | 7 +- go.sum | 7 +- internal/appsec/emitter/httpsec/http.go | 46 ---------- internal/appsec/emitter/waf/actions/block.go | 60 +++---------- .../emitter/waf/actions/http_redirect.go | 88 +------------------ 6 files changed, 34 insertions(+), 252 deletions(-) diff --git a/contrib/internal/httptrace/httptrace.go b/contrib/internal/httptrace/httptrace.go index 0c8a848b70..4aad5aba38 100644 --- a/contrib/internal/httptrace/httptrace.go +++ b/contrib/internal/httptrace/httptrace.go @@ -11,7 +11,6 @@ import ( "context" "fmt" "net/http" - "net/url" "strconv" "strings" @@ -28,29 +27,14 @@ var ( ) // StartRequestSpan starts an HTTP request span with the standard list of HTTP request span tags (http.method, http.url, -// http.useragent) with a http.Request object. Any further span start option can be added with opts. -func StartRequestSpan(r *http.Request, opts ...ddtrace.StartSpanOption) (tracer.Span, context.Context) { - return StartHttpSpan( - r.Context(), - r.Header, - r.Host, - r.Method, - urlFromRequest(r), - r.UserAgent(), - r.RemoteAddr, - opts..., - ) -} - -// StartHttpSpan starts an HTTP request span with the standard list of HTTP request span tags (http.method, http.url, // http.useragent). Any further span start option can be added with opts. -func StartHttpSpan(ctx context.Context, headers map[string][]string, host string, method string, url string, userAgent string, remoteAddr string, opts ...ddtrace.StartSpanOption) (tracer.Span, context.Context) { +func StartRequestSpan(r *http.Request, opts ...ddtrace.StartSpanOption) (tracer.Span, context.Context) { // Append our span options before the given ones so that the caller can "overwrite" them. // TODO(): rework span start option handling (https://github.com/DataDog/dd-trace-go/issues/1352) var ipTags map[string]string if cfg.traceClientIP { - ipTags, _ = httpsec.ClientIPTags(headers, true, remoteAddr) + ipTags, _ = httpsec.ClientIPTags(r.Header, true, r.RemoteAddr) } nopts := make([]ddtrace.StartSpanOption, 0, len(opts)+1+len(ipTags)) nopts = append(nopts, @@ -59,12 +43,12 @@ func StartHttpSpan(ctx context.Context, headers map[string][]string, host string cfg.Tags = make(map[string]interface{}) } cfg.Tags[ext.SpanType] = ext.SpanTypeWeb - cfg.Tags[ext.HTTPMethod] = method - cfg.Tags[ext.HTTPURL] = url - cfg.Tags[ext.HTTPUserAgent] = userAgent + cfg.Tags[ext.HTTPMethod] = r.Method + cfg.Tags[ext.HTTPURL] = urlFromRequest(r) + cfg.Tags[ext.HTTPUserAgent] = r.UserAgent() cfg.Tags["_dd.measured"] = 1 - if host != "" { - cfg.Tags["http.host"] = host + if r.Host != "" { + cfg.Tags["http.host"] = r.Host } if spanctx, err := tracer.Extract(tracer.HTTPHeadersCarrier(r.Header)); err == nil { // If there are span links as a result of context extraction, add them as a StartSpanOption @@ -73,13 +57,12 @@ func StartHttpSpan(ctx context.Context, headers map[string][]string, host string } tracer.ChildOf(spanctx)(cfg) } - for k, v := range ipTags { cfg.Tags[k] = v } }) nopts = append(nopts, opts...) - return tracer.StartSpanFromContext(ctx, namingschema.OpName(namingschema.HTTPServer), nopts...) + return tracer.StartSpanFromContext(r.Context(), namingschema.OpName(namingschema.HTTPServer), nopts...) } // FinishRequestSpan finishes the given HTTP request span and sets the expected response-related tags such as the status @@ -118,54 +101,27 @@ func urlFromRequest(r *http.Request) string { // "For most requests, fields other than Path and RawQuery will be // empty. (See RFC 7230, Section 5.3)" // This is why we don't rely on url.URL.String(), url.URL.Host, url.URL.Scheme, etc... + var url string + path := r.URL.EscapedPath() scheme := "http" if r.TLS != nil { scheme = "https" } - - return urlFromArgs( - r.URL.EscapedPath(), - scheme, - r.Host, - r.URL.RawQuery, - r.URL.EscapedFragment(), - ) -} - -// UrlFromUrl returns the full URL from a URL object. If query params are collected, they are obfuscated granted -// obfuscation is not disabled by the user (through DD_TRACE_OBFUSCATION_QUERY_STRING_REGEXP) -// See https://docs.datadoghq.com/tracing/configure_data_security#redacting-the-query-in-the-url for more information. -func UrlFromUrl(u *url.URL) string { - scheme := "http" - if u.Scheme != "" { - scheme = u.Scheme - } - return urlFromArgs( - u.EscapedPath(), - scheme, - u.Host, - u.RawQuery, - u.EscapedFragment(), - ) -} - -func urlFromArgs(escapedPath string, scheme string, host string, rawQuery string, escapedFragment string) string { - var url string - if host != "" { - url = strings.Join([]string{scheme, "://", host, escapedPath}, "") + if r.Host != "" { + url = strings.Join([]string{scheme, "://", r.Host, path}, "") } else { - url = escapedPath + url = path } // Collect the query string if we are allowed to report it and obfuscate it if possible/allowed - if cfg.queryString && rawQuery != "" { - query := rawQuery + if cfg.queryString && r.URL.RawQuery != "" { + query := r.URL.RawQuery if cfg.queryStringRegexp != nil { query = cfg.queryStringRegexp.ReplaceAllLiteralString(query, "") } url = strings.Join([]string{url, query}, "?") } - if escapedFragment != "" { - url = strings.Join([]string{url, escapedFragment}, "#") + if frag := r.URL.EscapedFragment(); frag != "" { + url = strings.Join([]string{url, frag}, "#") } return url } diff --git a/go.mod b/go.mod index c28ee16637..1c7d462f96 100644 --- a/go.mod +++ b/go.mod @@ -40,7 +40,7 @@ require ( github.com/elastic/go-elasticsearch/v8 v8.4.0 github.com/emicklei/go-restful v2.16.0+incompatible github.com/emicklei/go-restful/v3 v3.11.0 - github.com/envoyproxy/go-control-plane v0.13.0 + github.com/envoyproxy/go-control-plane v0.12.0 github.com/garyburd/redigo v1.6.4 github.com/gin-gonic/gin v1.9.1 github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8 @@ -103,8 +103,8 @@ require ( golang.org/x/sys v0.24.0 golang.org/x/time v0.6.0 golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 - google.golang.org/api v0.169.0 - google.golang.org/grpc v1.65.0 + google.golang.org/api v0.192.0 + google.golang.org/grpc v1.64.1 google.golang.org/protobuf v1.34.2 gopkg.in/jinzhu/gorm.v1 v1.9.2 gopkg.in/olivere/elastic.v3 v3.0.75 @@ -243,7 +243,6 @@ require ( github.com/philhofer/fwd v1.1.3-0.20240612014219-fbbf4953d986 // indirect github.com/pierrec/lz4/v4 v4.1.18 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c // indirect github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect diff --git a/go.sum b/go.sum index 8d87232f52..0d575fea1b 100644 --- a/go.sum +++ b/go.sum @@ -1121,8 +1121,8 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.m github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= github.com/envoyproxy/go-control-plane v0.10.3/go.mod h1:fJJn/j26vwOu972OllsvAgJJM//w9BV6Fxbg2LuVd34= -github.com/envoyproxy/go-control-plane v0.13.0 h1:HzkeUz1Knt+3bK+8LG1bxOO/jzWZmdxpwC51i202les= -github.com/envoyproxy/go-control-plane v0.13.0/go.mod h1:GRaKG3dwvFoTg4nj7aXdZnvMg4d7nvT/wl9WgVXn3Q8= +github.com/envoyproxy/go-control-plane v0.12.0 h1:4X+VP1GHd1Mhj6IB5mMeGbLCleqxjletLK6K0rbxyZI= +github.com/envoyproxy/go-control-plane v0.12.0/go.mod h1:ZBTaoJ23lqITozF0M6G4/IragXCQKCnYbmlmtHvwRG0= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo= github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w= @@ -1890,8 +1890,6 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= -github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= -github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -3067,7 +3065,6 @@ google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwS google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= -google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= diff --git a/internal/appsec/emitter/httpsec/http.go b/internal/appsec/emitter/httpsec/http.go index 2602b6ff16..41ebfa7e23 100644 --- a/internal/appsec/emitter/httpsec/http.go +++ b/internal/appsec/emitter/httpsec/http.go @@ -12,12 +12,9 @@ package httpsec import ( "context" - "strings" - // Blank import needed to use embed for the default blocked response payloads _ "embed" "net/http" - "net/url" "sync/atomic" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace" @@ -200,46 +197,3 @@ func WrapHandler(handler http.Handler, span ddtrace.Span, pathParams map[string] handler.ServeHTTP(tw, tr) }) } - -// MakeHandlerOperationArgs creates the HandlerOperationArgs value. -func MakeHandlerOperationArgs(headers map[string][]string, method string, host string, remoteAddr string, url *url.URL) HandlerOperationArgs { - cookies := filterCookiesFromHeaders(headers) - - args := HandlerOperationArgs{ - Method: method, - RequestURI: url.RequestURI(), - Host: host, - RemoteAddr: remoteAddr, - Headers: headers, - Cookies: cookies, - QueryParams: url.Query(), - PathParams: map[string]string{}, - } - - return args -} - -// Separate the cookies from the headers, return the parsed cookies and remove in place the cookies from the headers. -// Headers used for `server.request.headers.no_cookies` and `server.response.headers.no_cookies` addresses for the WAF -// Cookies are used for the `server.request.cookies` address -func filterCookiesFromHeaders(headers http.Header) map[string][]string { - cookieHeader, ok := headers["Cookie"] - if !ok { - return make(http.Header) - } - - delete(headers, "Cookie") - - cookies := make(map[string][]string, len(cookieHeader)) - for _, c := range cookieHeader { - parts := strings.Split(c, ";") - for _, part := range parts { - cookie := strings.Split(part, "=") - if len(cookie) == 2 { - cookies[cookie[0]] = append(cookies[cookie[0]], cookie[1]) - } - } - } - - return cookies -} diff --git a/internal/appsec/emitter/waf/actions/block.go b/internal/appsec/emitter/waf/actions/block.go index 40418f4410..ae802b60bd 100644 --- a/internal/appsec/emitter/waf/actions/block.go +++ b/internal/appsec/emitter/waf/actions/block.go @@ -69,11 +69,6 @@ type ( // BlockHTTP are actions that interact with an HTTP request flow BlockHTTP struct { http.Handler - - StatusCode int `mapstructure:"status_code"` - RedirectLocation string - // BlockingTemplate is a function that returns the headers to be added and body to be written to the response - BlockingTemplate func(headers map[string][]string) (map[string][]string, []byte) } ) @@ -130,11 +125,7 @@ func NewBlockAction(params map[string]any) []Action { } func newHTTPBlockRequestAction(status int, template string) *BlockHTTP { - return &BlockHTTP{ - Handler: newBlockHandler(status, template), - BlockingTemplate: newManualBlockHandler(template), - StatusCode: status, - } + return &BlockHTTP{Handler: newBlockHandler(status, template)} } // newBlockHandler creates, initializes and returns a new BlockRequestAction @@ -148,47 +139,16 @@ func newBlockHandler(status int, template string) http.Handler { return htmlHandler default: return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - h := findCorrectTemplate(jsonHandler, htmlHandler, r.Header.Get("Accept")) - h.(http.Handler).ServeHTTP(w, r) - }) - } -} - -func newManualBlockHandler(template string) func(headers map[string][]string) (map[string][]string, []byte) { - htmlHandler := newManualBlockDataHandler("text/html", blockedTemplateHTML) - jsonHandler := newManualBlockDataHandler("application/json", blockedTemplateJSON) - switch template { - case "json": - return jsonHandler - case "html": - return htmlHandler - default: - return func(headers map[string][]string) (map[string][]string, []byte) { - acceptHeader := "" - if hdr, ok := headers["Accept"]; ok && len(hdr) > 0 { - acceptHeader = hdr[0] + h := jsonHandler + hdr := r.Header.Get("Accept") + htmlIdx := strings.Index(hdr, "text/html") + jsonIdx := strings.Index(hdr, "application/json") + // Switch to html handler if text/html comes before application/json in the Accept header + if htmlIdx != -1 && (jsonIdx == -1 || htmlIdx < jsonIdx) { + h = htmlHandler } - h := findCorrectTemplate(jsonHandler, htmlHandler, acceptHeader) - return h.(func(headers map[string][]string) (map[string][]string, []byte))(headers) - } - } -} - -func findCorrectTemplate(jsonHandler interface{}, htmlHandler interface{}, acceptHeader string) interface{} { - h := jsonHandler - hdr := acceptHeader - htmlIdx := strings.Index(hdr, "text/html") - jsonIdx := strings.Index(hdr, "application/json") - // Switch to html handler if text/html comes before application/json in the Accept header - if htmlIdx != -1 && (jsonIdx == -1 || htmlIdx < jsonIdx) { - h = htmlHandler - } - return h -} - -func newManualBlockDataHandler(ct string, template []byte) func(headers map[string][]string) (map[string][]string, []byte) { - return func(headers map[string][]string) (map[string][]string, []byte) { - return map[string][]string{"Content-Type": {ct}}, template + h.ServeHTTP(w, r) + }) } } diff --git a/internal/appsec/emitter/waf/actions/http_redirect.go b/internal/appsec/emitter/waf/actions/http_redirect.go index 25e209d790..3cdca4c818 100644 --- a/internal/appsec/emitter/waf/actions/http_redirect.go +++ b/internal/appsec/emitter/waf/actions/http_redirect.go @@ -7,14 +7,10 @@ package actions import ( "net/http" - "path" - "strings" "github.com/mitchellh/mapstructure" "gopkg.in/DataDog/dd-trace-go.v1/internal/log" - - urlpkg "net/url" ) // redirectActionParams are the dynamic parameters to be provided to a "redirect_request" @@ -42,17 +38,9 @@ func newRedirectRequestAction(status int, loc string) *BlockHTTP { // If location is not set we fall back on a default block action if loc == "" { - return &BlockHTTP{ - Handler: newBlockHandler(http.StatusForbidden, string(blockedTemplateJSON)), - StatusCode: status, - BlockingTemplate: newManualBlockHandler("json"), - } - } - return &BlockHTTP{ - Handler: http.RedirectHandler(loc, status), - StatusCode: status, - RedirectLocation: loc, + return &BlockHTTP{Handler: newBlockHandler(http.StatusForbidden, string(blockedTemplateJSON))} } + return &BlockHTTP{Handler: http.RedirectHandler(loc, status)} } // NewRedirectAction creates an action for the "redirect_request" action type @@ -64,75 +52,3 @@ func NewRedirectAction(params map[string]any) []Action { } return []Action{newRedirectRequestAction(p.StatusCode, p.Location)} } - -// HandleRedirectLocationString returns the headers and body to be written to the response when a redirect is needed -// Vendored from net/http/server.go -func HandleRedirectLocationString(oldpath string, url string, statusCode int, method string, h map[string][]string) (map[string][]string, []byte) { - if u, err := urlpkg.Parse(url); err == nil { - // If url was relative, make its path absolute by - // combining with request path. - // The client would probably do this for us, - // but doing it ourselves is more reliable. - // See RFC 7231, section 7.1.2 - if u.Scheme == "" && u.Host == "" { - if oldpath == "" { // should not happen, but avoid a crash if it does - oldpath = "/" - } - - // no leading http://server - if url == "" || url[0] != '/' { - // make relative path absolute - olddir, _ := path.Split(oldpath) - url = olddir + url - } - - var query string - if i := strings.Index(url, "?"); i != -1 { - url, query = url[:i], url[i:] - } - - // clean up but preserve trailing slash - trailing := strings.HasSuffix(url, "/") - url = path.Clean(url) - if trailing && !strings.HasSuffix(url, "/") { - url += "/" - } - url += query - } - } - - // RFC 7231 notes that a short HTML body is usually included in - // the response because older user agents may not understand 301/307. - // Do it only if the request didn't already have a Content-Type header. - _, hadCT := h["content-type"] - newHeaders := make(map[string][]string, 2) - - newHeaders["location"] = []string{url} - if !hadCT && (method == "GET" || method == "HEAD") { - newHeaders["content-length"] = []string{"text/html; charset=utf-8"} - } - - // Shouldn't send the body for POST or HEAD; that leaves GET. - var body []byte - if !hadCT && method == "GET" { - body = []byte("" + http.StatusText(statusCode) + ".\n") - } - - return newHeaders, body -} - -// Vendored from net/http/server.go -var htmlReplacer = strings.NewReplacer( - "&", "&", - "<", "<", - ">", ">", - // """ is shorter than """. - `"`, """, - // "'" is shorter than "'" and apos was not in HTML until HTML5. - "'", "'", -) - -// htmlEscape escapes special characters like "<" to become "<". -func htmlEscape(s string) string { - return htmlReplacer.Replace(s) -} From e210f055fa29cf7eaea220f0d02d67bed2db3098 Mon Sep 17 00:00:00 2001 From: Eliott Bouhana Date: Fri, 8 Nov 2024 19:04:04 +0100 Subject: [PATCH 06/14] rework to fake a new http.Request and a http.ResponseWriter * Add support for context propagation * Normalize span tag use Co-authored-by: Flavien Darche Signed-off-by: Eliott Bouhana --- contrib/envoyproxy/envoy/envoy.go | 432 +++++++----------- contrib/envoyproxy/envoy/envoy_test.go | 99 ++-- contrib/envoyproxy/envoy/fakehttp.go | 189 ++++++++ contrib/internal/httptrace/response_writer.go | 7 + go.mod | 5 +- go.sum | 6 +- 6 files changed, 425 insertions(+), 313 deletions(-) create mode 100644 contrib/envoyproxy/envoy/fakehttp.go diff --git a/contrib/envoyproxy/envoy/envoy.go b/contrib/envoyproxy/envoy/envoy.go index ca407bba43..6805c1b73a 100644 --- a/contrib/envoyproxy/envoy/envoy.go +++ b/contrib/envoyproxy/envoy/envoy.go @@ -8,84 +8,69 @@ package envoy import ( "context" "errors" - "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry" "io" "math" "net/http" - "net/url" - "strconv" "strings" - "sync/atomic" - corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" - v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" - extproc "github.com/envoyproxy/go-control-plane/envoy/service/ext_proc/v3" - v32 "github.com/envoyproxy/go-control-plane/envoy/type/v3" - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/metadata" - "google.golang.org/grpc/status" grpctrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/google.golang.org/grpc" + "gopkg.in/DataDog/dd-trace-go.v1/contrib/internal/httptrace" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext" - "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/emitter/waf/actions" - - "gopkg.in/DataDog/dd-trace-go.v1/contrib/internal/httptrace" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" - "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/emitter/httpsec" - httpsec2 "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/listener/httpsec" + "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/dyngo" + "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/emitter/waf/actions" "gopkg.in/DataDog/dd-trace-go.v1/internal/log" + "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry" + + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + envoycore "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" + envoyextproc "github.com/envoyproxy/go-control-plane/envoy/service/ext_proc/v3" + envoytypes "github.com/envoyproxy/go-control-plane/envoy/type/v3" ) -const componentName = "envoy/service/ext_proc/v3" +const componentName = "envoyproxy/go-control-plane/envoy/service/ext_proc/envoycore" func init() { telemetry.LoadIntegration(componentName) - tracer.MarkIntegrationImported("github.com/envoyproxy/go-control-plane/envoy/service/ext_proc/v3") + tracer.MarkIntegrationImported("github.com/envoyproxy/go-control-plane/envoy/service/ext_proc/envoycore") } type CurrentRequest struct { - op *httpsec.HandlerOperation - blockAction *atomic.Pointer[actions.BlockHTTP] - span tracer.Span - - remoteAddr string - parsedUrl *url.URL - requestArgs httpsec.HandlerOperationArgs - - statusCode int - blocked bool -} - -func getRemoteAddr(xfwd []string) string { - length := len(xfwd) - if length == 0 { - return "" - } - - // Get the first right value of x-forwarded-for header - // The rightmost IP address is the one that will be used as the remote client IP - // https://datadoghq.atlassian.net/wiki/spaces/TS/pages/2766733526/Sensitive+IP+information#Where-does-the-value-of-the-http.client_ip-tag-come-from%3F - return xfwd[length-1] + span tracer.Span + afterHandle func() + ctx context.Context + fakeResponseWriter *FakeResponseWriter + wrappedResponseWriter http.ResponseWriter } func StreamServerInterceptor(opts ...grpctrace.Option) grpc.StreamServerInterceptor { interceptor := grpctrace.StreamServerInterceptor(opts...) return func(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { - if info.FullMethod != extproc.ExternalProcessor_Process_FullMethodName { + if info.FullMethod != envoyextproc.ExternalProcessor_Process_FullMethodName { return interceptor(srv, ss, info, handler) } - ctx := ss.Context() - md, _ := metadata.FromIncomingContext(ctx) - currentRequest := &CurrentRequest{ - blocked: false, - remoteAddr: getRemoteAddr(md.Get("x-forwarded-for")), - } + var ( + ctx = ss.Context() + blocked bool + currentRequest *CurrentRequest + processingRequest envoyextproc.ProcessingRequest + processingResponse *envoyextproc.ProcessingResponse + ) // Close the span when the request is done processing - defer closeSpan(currentRequest) + defer func() { + if currentRequest != nil { + log.Warn("external_processing: stream stopped during a request, making sure the current span is closed\n") + currentRequest.span.Finish() + currentRequest = nil + } + }() for { select { @@ -99,8 +84,8 @@ func StreamServerInterceptor(opts ...grpctrace.Option) grpc.StreamServerIntercep default: } - var req extproc.ProcessingRequest - if err := ss.RecvMsg(&req); err != nil { + err := ss.RecvMsg(&processingRequest) + 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 { @@ -111,59 +96,69 @@ func StreamServerInterceptor(opts ...grpctrace.Option) grpc.StreamServerIntercep return status.Errorf(codes.Unknown, "Error receiving request/response: %v", err) } - resp, err := envoyExternalProcessingEventHandler(ctx, &req, currentRequest) + processingResponse, err = envoyExternalProcessingRequestTypeAssert(&processingRequest) + if err != nil { + log.Error("external_processing: error asserting request type: %v\n", err) + return status.Errorf(codes.Unknown, "Error asserting request type: %v", err) + } + + switch v := processingRequest.Request.(type) { + case *envoyextproc.ProcessingRequest_RequestHeaders: + processingResponse, currentRequest, blocked, err = ProcessRequestHeaders(ctx, v) + case *envoyextproc.ProcessingRequest_ResponseHeaders: + processingResponse, err = ProcessResponseHeaders(v, currentRequest) + currentRequest = nil // Request is done, reset the current request + } + if err != nil { - log.Error("external_processing: error processing request/response: %v\n", err) - return status.Errorf(codes.Unknown, "Error processing request/response: %v", err) + log.Error("external_processing: error processing request: %v\n", err) + return err } // End of stream reached, no more data to process - if resp == nil { + if processingResponse == nil { log.Debug("external_processing: end of stream reached") return nil } - // Send Message could fail if envoy close the stream before the message could be sent (probably because of an Envoy timeout) - if err := ss.SendMsg(resp); err != nil { + if err := ss.SendMsg(processingResponse); err != nil { log.Warn("external_processing: error sending response (probably because of an Envoy timeout): %v", err) return status.Errorf(codes.Unknown, "Error sending response (probably because of an Envoy timeout): %v", err) } - if currentRequest.blocked { - log.Debug("external_processing: request blocked, stream ended") + if blocked { + log.Debug("external_processing: request blocked, end the stream") + currentRequest = nil return nil } } } } -func envoyExternalProcessingEventHandler(ctx context.Context, req *extproc.ProcessingRequest, currentRequest *CurrentRequest) (*extproc.ProcessingResponse, error) { +func envoyExternalProcessingRequestTypeAssert(req *envoyextproc.ProcessingRequest) (*envoyextproc.ProcessingResponse, error) { switch v := req.Request.(type) { - case *extproc.ProcessingRequest_RequestHeaders: - return ProcessRequestHeaders(ctx, req.Request.(*extproc.ProcessingRequest_RequestHeaders), currentRequest) + case *envoyextproc.ProcessingRequest_RequestHeaders, *envoyextproc.ProcessingRequest_ResponseHeaders: + return nil, nil - case *extproc.ProcessingRequest_RequestBody: + case *envoyextproc.ProcessingRequest_RequestBody: // TODO: Handle request raw body in the WAF - return &extproc.ProcessingResponse{ - Response: &extproc.ProcessingResponse_RequestBody{ - RequestBody: &extproc.BodyResponse{ - Response: &extproc.CommonResponse{ - Status: extproc.CommonResponse_CONTINUE, + return &envoyextproc.ProcessingResponse{ + Response: &envoyextproc.ProcessingResponse_RequestBody{ + RequestBody: &envoyextproc.BodyResponse{ + Response: &envoyextproc.CommonResponse{ + Status: envoyextproc.CommonResponse_CONTINUE, }, }, }, }, nil - case *extproc.ProcessingRequest_RequestTrailers: - return &extproc.ProcessingResponse{ - Response: &extproc.ProcessingResponse_RequestTrailers{}, + case *envoyextproc.ProcessingRequest_RequestTrailers: + return &envoyextproc.ProcessingResponse{ + Response: &envoyextproc.ProcessingResponse_RequestTrailers{}, }, nil - case *extproc.ProcessingRequest_ResponseHeaders: - return ProcessResponseHeaders(req.Request.(*extproc.ProcessingRequest_ResponseHeaders), currentRequest) - - case *extproc.ProcessingRequest_ResponseBody: - r := req.Request.(*extproc.ProcessingRequest_ResponseBody) + case *envoyextproc.ProcessingRequest_ResponseBody: + r := req.Request.(*envoyextproc.ProcessingRequest_ResponseBody) // Note: The end of stream bool value is not reliable // Sometimes it's not set to true even if there is no more data to process @@ -172,13 +167,13 @@ func envoyExternalProcessingEventHandler(ctx context.Context, req *extproc.Proce } // TODO: Handle response raw body in the WAF - return &extproc.ProcessingResponse{ - Response: &extproc.ProcessingResponse_ResponseBody{}, + return &envoyextproc.ProcessingResponse{ + Response: &envoyextproc.ProcessingResponse_ResponseBody{}, }, nil - case *extproc.ProcessingRequest_ResponseTrailers: - return &extproc.ProcessingResponse{ - Response: &extproc.ProcessingResponse_RequestTrailers{}, + case *envoyextproc.ProcessingRequest_ResponseTrailers: + return &envoyextproc.ProcessingResponse{ + Response: &envoyextproc.ProcessingResponse_RequestTrailers{}, }, nil default: @@ -186,241 +181,158 @@ func envoyExternalProcessingEventHandler(ctx context.Context, req *extproc.Proce } } -func ProcessRequestHeaders(ctx context.Context, req *extproc.ProcessingRequest_RequestHeaders, currentRequest *CurrentRequest) (*extproc.ProcessingResponse, error) { +func ProcessRequestHeaders(ctx context.Context, req *envoyextproc.ProcessingRequest_RequestHeaders) (*envoyextproc.ProcessingResponse, *CurrentRequest, bool, error) { log.Debug("external_processing: received request headers: %v\n", req.RequestHeaders) - headers, envoyHeaders := separateEnvoyHeaders(req.RequestHeaders.GetHeaders().GetHeaders()) - - // Create args - host, scheme, path, method, err := verifyRequestHttp2RequestHeaders(envoyHeaders) - if err != nil { - return nil, err - } - - requestURI := scheme + "://" + host + path - parsedUrl, err := url.Parse(requestURI) + request, err := NewRequestFromExtProc(ctx, req) if err != nil { - return nil, status.Errorf(codes.InvalidArgument, "Error parsing request URI: %v", err) + return nil, nil, false, status.Errorf(codes.InvalidArgument, "Error processing request headers from ext_proc: %v", err) } - currentRequest.parsedUrl = parsedUrl - // client ip set in the x-forwarded-for header (cf: https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_conn_man/headers#x-forwarded-for) - ipTags, _ := httpsec2.ClientIPTags(headers, true, currentRequest.remoteAddr) - - currentRequest.requestArgs = httpsec.MakeHandlerOperationArgs(headers, method, host, currentRequest.remoteAddr, parsedUrl) - headers = currentRequest.requestArgs.Headers // Replace headers with the ones from the args because it has been modified - - // Create span - currentRequest.span = createExternalProcessedSpan(ctx, headers, method, host, path, currentRequest.remoteAddr, ipTags, parsedUrl) - - // Run WAF on request data - currentRequest.op, currentRequest.blockAction, _ = httpsec.StartOperation(ctx, currentRequest.requestArgs) + var blocked bool + fakeResponseWriter := NewFakeResponseWriter() + wrappedResponseWriter, request, afterHandle, blocked := httptrace.BeforeHandle(&httptrace.ServeConfig{ + SpanOpts: []ddtrace.StartSpanOption{ + tracer.Tag(ext.SpanKind, ext.SpanKindServer), + tracer.Tag(ext.Component, componentName), + }, + }, fakeResponseWriter, request) // Block handling: If triggered, we need to block the request, return an immediate response - if blockPtr := currentRequest.blockAction.Swap(nil); blockPtr != nil { - response := doBlockRequest(currentRequest, blockPtr, headers) - return response, nil + if blocked { + afterHandle() + return doBlockResponse(fakeResponseWriter), nil, true, nil } - return &extproc.ProcessingResponse{ - Response: &extproc.ProcessingResponse_RequestHeaders{ - RequestHeaders: &extproc.HeadersResponse{ - Response: &extproc.CommonResponse{ - Status: extproc.CommonResponse_CONTINUE, - }, - }, - }, - }, nil -} - -// Verify the required HTTP2 headers are present -// Some mandatory headers need to be set. It can happen when it wasn't a real HTTP2 request sent by Envoy, -func verifyRequestHttp2RequestHeaders(headers map[string][]string) (string, string, string, string, error) { - // :authority, :scheme, :path, :method + span, ok := tracer.SpanFromContext(request.Context()) + if !ok { + return nil, nil, false, status.Errorf(codes.Unknown, "Error getting span from context") + } - for _, header := range []string{":authority", ":scheme", ":path", ":method"} { - if _, ok := headers[header]; !ok || len(headers[header]) == 0 { - return "", "", "", "", status.Errorf(codes.InvalidArgument, "Missing required header: %v", header) - } + processingResponse, err := propagationRequestHeaderMutation(span) + if err != nil { + return nil, nil, false, err } - return headers[":authority"][0], headers[":scheme"][0], headers[":path"][0], headers[":method"][0], nil + return processingResponse, &CurrentRequest{ + span: span, + ctx: request.Context(), + fakeResponseWriter: fakeResponseWriter, + wrappedResponseWriter: wrappedResponseWriter, + afterHandle: afterHandle, + }, false, nil } -func verifyRequestHttp2ResponseHeaders(headers map[string][]string) (string, error) { - // :status +func propagationRequestHeaderMutation(span ddtrace.Span) (*envoyextproc.ProcessingResponse, error) { + newHeaders := make(http.Header) + if err := tracer.Inject(span.Context(), tracer.HTTPHeadersCarrier(newHeaders)); err != nil { + return nil, status.Errorf(codes.Unknown, "Error injecting headers: %v", err) + } + + if len(newHeaders) > 0 { + log.Debug("external_processing: injecting propagation headers: %v\n", newHeaders) + } - if _, ok := headers[":status"]; !ok || len(headers[":status"]) == 0 { - return "", status.Errorf(codes.InvalidArgument, "Missing required header: %v", ":status") + headerValueOptions := make([]*envoycore.HeaderValueOption, 0, len(newHeaders)) + for k, v := range newHeaders { + headerValueOptions = append(headerValueOptions, &envoycore.HeaderValueOption{ + Header: &envoycore.HeaderValue{ + Key: k, + RawValue: []byte(strings.Join(v, ",")), + }, + }) } - return headers[":status"][0], nil + return &envoyextproc.ProcessingResponse{ + Response: &envoyextproc.ProcessingResponse_RequestHeaders{ + RequestHeaders: &envoyextproc.HeadersResponse{ + Response: &envoyextproc.CommonResponse{ + Status: envoyextproc.CommonResponse_CONTINUE, + HeaderMutation: &envoyextproc.HeaderMutation{ + SetHeaders: headerValueOptions, + }, + }, + }, + }, + }, nil } -func ProcessResponseHeaders(res *extproc.ProcessingRequest_ResponseHeaders, currentRequest *CurrentRequest) (*extproc.ProcessingResponse, error) { +func ProcessResponseHeaders(res *envoyextproc.ProcessingRequest_ResponseHeaders, currentRequest *CurrentRequest) (*envoyextproc.ProcessingResponse, error) { log.Debug("external_processing: received response headers: %v\n", res.ResponseHeaders) - headers, envoyHeaders := separateEnvoyHeaders(res.ResponseHeaders.GetHeaders().GetHeaders()) - - statusCodeStr, err := verifyRequestHttp2ResponseHeaders(envoyHeaders) - if err != nil { - return nil, err + if err := NewFakeResponseWriterFromExtProc(currentRequest.wrappedResponseWriter, res); err != nil { + return nil, status.Errorf(codes.InvalidArgument, "Error processing response headers from ext_proc: %v", err) } - currentRequest.statusCode, err = strconv.Atoi(statusCodeStr) - if err != nil { - return nil, status.Errorf(codes.InvalidArgument, "Error parsing response header status code: %v", err) - } + var blocked bool - args := httpsec.HandlerOperationRes{ - Headers: headers, - StatusCode: currentRequest.statusCode, + // Now we need to know if the request has been blocked, but we don't have any other way than to look for the operation and bind a blocking data listener to it + op, ok := dyngo.FromContext(currentRequest.ctx) + if ok { + dyngo.OnData(op, func(_ *actions.BlockHTTP) { + // We already wrote over the response writer, we need to reset it so the blocking handler can write to it + httptrace.ResetStatusCode(currentRequest.wrappedResponseWriter) + currentRequest.fakeResponseWriter.Reset() + blocked = true + }) } - currentRequest.op.Finish(args, currentRequest.span) - currentRequest.op = nil + currentRequest.afterHandle() - // Block handling: If triggered, we need to block the request, return an immediate response - if blockPtr := currentRequest.blockAction.Swap(nil); blockPtr != nil { - return doBlockRequest(currentRequest, blockPtr, headers), nil + if blocked { + response := doBlockResponse(currentRequest.fakeResponseWriter) + return response, nil } + log.Debug("external_processing: finishing request with status code: %v\n", currentRequest.fakeResponseWriter.status) + // Note: (cf. comment in the stream error handling) // The end of stream bool value is not reliable if res.ResponseHeaders.GetEndOfStream() { return nil, nil } - return &extproc.ProcessingResponse{ - Response: &extproc.ProcessingResponse_ResponseHeaders{ - ResponseHeaders: &extproc.HeadersResponse{ - Response: &extproc.CommonResponse{ - Status: extproc.CommonResponse_CONTINUE, + return &envoyextproc.ProcessingResponse{ + Response: &envoyextproc.ProcessingResponse_ResponseHeaders{ + ResponseHeaders: &envoyextproc.HeadersResponse{ + Response: &envoyextproc.CommonResponse{ + Status: envoyextproc.CommonResponse_CONTINUE, }, }, }, }, nil } -func createExternalProcessedSpan(ctx context.Context, headers map[string][]string, method string, host string, path string, remoteAddr string, ipTags map[string]string, parsedUrl *url.URL) tracer.Span { - userAgent := "" - if ua, ok := headers["User-Agent"]; ok || len(ua) > 0 { - userAgent = ua[0] - } - - span, _ := httptrace.StartHttpSpan( - ctx, - headers, - host, - method, - httptrace.UrlFromUrl(parsedUrl), - userAgent, - remoteAddr, - []ddtrace.StartSpanOption{ - func(cfg *ddtrace.StartSpanConfig) { - cfg.Tags[ext.ResourceName] = method + " " + path - cfg.Tags[ext.SpanKind] = ext.SpanKindServer - - // Add client IP tags - for k, v := range ipTags { - cfg.Tags[k] = v - } - }, - }..., - ) - - return span -} - -// Separate normal headers of the initial request made by the client and the pseudo headers of HTTP/2 -// - Format the headers to be used by the tracer as a map[string][]string -// - Set header keys to be canonical -func separateEnvoyHeaders(receivedHeaders []*corev3.HeaderValue) (map[string][]string, map[string][]string) { - headers := make(map[string][]string) - pseudoHeadersHttp2 := make(map[string][]string) - for _, v := range receivedHeaders { - key := v.GetKey() - if len(key) == 0 { - continue - } - - if key[0] == ':' { - pseudoHeadersHttp2[key] = []string{string(v.GetRawValue())} - } else { - headers[http.CanonicalHeaderKey(key)] = []string{string(v.GetRawValue())} - } - } - return headers, pseudoHeadersHttp2 -} - -func doBlockRequest(currentRequest *CurrentRequest, blockAction *actions.BlockHTTP, headers map[string][]string) *extproc.ProcessingResponse { - currentRequest.blocked = true - - var headerToSet map[string][]string - var body []byte - if blockAction.RedirectLocation != "" { - headerToSet, body = actions.HandleRedirectLocationString( - currentRequest.parsedUrl.Path, - blockAction.RedirectLocation, - blockAction.StatusCode, - currentRequest.requestArgs.Method, - currentRequest.requestArgs.Headers, - ) - } else { - headerToSet, body = blockAction.BlockingTemplate(headers) - } - - var headersMutation []*v3.HeaderValueOption - for k, v := range headerToSet { - headersMutation = append(headersMutation, &v3.HeaderValueOption{ - Header: &v3.HeaderValue{ +func doBlockResponse(writer *FakeResponseWriter) *envoyextproc.ProcessingResponse { + var headersMutation []*envoycore.HeaderValueOption + for k, v := range writer.headers { + headersMutation = append(headersMutation, &envoycore.HeaderValueOption{ + Header: &envoycore.HeaderValue{ Key: k, RawValue: []byte(strings.Join(v, ",")), }, }) } - currentRequest.statusCode = blockAction.StatusCode var int32StatusCode int32 = 0 - if currentRequest.statusCode > 0 && currentRequest.statusCode <= math.MaxInt32 { - int32StatusCode = int32(currentRequest.statusCode) + if writer.status > 0 && writer.status <= math.MaxInt32 { + int32StatusCode = int32(writer.status) } - return &extproc.ProcessingResponse{ - Response: &extproc.ProcessingResponse_ImmediateResponse{ - ImmediateResponse: &extproc.ImmediateResponse{ - Status: &v32.HttpStatus{ - Code: v32.StatusCode(int32StatusCode), + return &envoyextproc.ProcessingResponse{ + Response: &envoyextproc.ProcessingResponse_ImmediateResponse{ + ImmediateResponse: &envoyextproc.ImmediateResponse{ + Status: &envoytypes.HttpStatus{ + Code: envoytypes.StatusCode(int32StatusCode), }, - Headers: &extproc.HeaderMutation{ + Headers: &envoyextproc.HeaderMutation{ SetHeaders: headersMutation, }, - Body: body, - GrpcStatus: &extproc.GrpcStatus{ + Body: writer.body, + GrpcStatus: &envoyextproc.GrpcStatus{ Status: 0, }, }, }, } } - -func closeSpan(currentRequest *CurrentRequest) { - span := currentRequest.span - if span != nil { - // Finish the operation: it can be not finished when the request has been blocked or if an error occurred - // > The response hasn't been processed - if currentRequest.op != nil { - currentRequest.op.Finish(httpsec.HandlerOperationRes{}, span) - currentRequest.op = nil - } - - // Note: The status code could be 0 if an internal error occurred - statusCodeStr := strconv.Itoa(currentRequest.statusCode) - span.SetTag(ext.HTTPCode, statusCodeStr) - - span.Finish() - - log.Debug("external_processing: span closed with status code: %v\n", currentRequest.statusCode) - currentRequest.span = nil - } -} diff --git a/contrib/envoyproxy/envoy/envoy_test.go b/contrib/envoyproxy/envoy/envoy_test.go index a1cea81a3a..d8860670df 100644 --- a/contrib/envoyproxy/envoy/envoy_test.go +++ b/contrib/envoyproxy/envoy/envoy_test.go @@ -15,8 +15,8 @@ import ( "net" "testing" - extproc "github.com/envoyproxy/go-control-plane/envoy/service/ext_proc/v3" - typev3 "github.com/envoyproxy/go-control-plane/envoy/type/v3" + envoyextproc "github.com/envoyproxy/go-control-plane/envoy/service/ext_proc/v3" + envoytypes "github.com/envoyproxy/go-control-plane/envoy/type/v3" ddgrpc "gopkg.in/DataDog/dd-trace-go.v1/contrib/google.golang.org/grpc" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/mocktracer" @@ -27,12 +27,12 @@ import ( "google.golang.org/grpc" ) -func end2EndStreamRequest(t *testing.T, stream extproc.ExternalProcessor_ProcessClient, path string, method string, requestHeaders map[string]string, responseHeaders map[string]string, blockOnResponse bool) { +func end2EndStreamRequest(t *testing.T, stream envoyextproc.ExternalProcessor_ProcessClient, path string, method string, requestHeaders map[string]string, responseHeaders map[string]string, blockOnResponse bool) { // First part: request // 1- Send the headers - err := stream.Send(&extproc.ProcessingRequest{ - Request: &extproc.ProcessingRequest_RequestHeaders{ - RequestHeaders: &extproc.HttpHeaders{ + err := stream.Send(&envoyextproc.ProcessingRequest{ + Request: &envoyextproc.ProcessingRequest_RequestHeaders{ + RequestHeaders: &envoyextproc.HttpHeaders{ Headers: makeRequestHeaders(requestHeaders, method, path), }, }, @@ -41,12 +41,12 @@ func end2EndStreamRequest(t *testing.T, stream extproc.ExternalProcessor_Process res, err := stream.Recv() require.NoError(t, err) - require.Equal(t, extproc.CommonResponse_CONTINUE, res.GetRequestHeaders().GetResponse().GetStatus()) + require.Equal(t, envoyextproc.CommonResponse_CONTINUE, res.GetRequestHeaders().GetResponse().GetStatus()) // 2- Send the body - err = stream.Send(&extproc.ProcessingRequest{ - Request: &extproc.ProcessingRequest_RequestBody{ - RequestBody: &extproc.HttpBody{ + err = stream.Send(&envoyextproc.ProcessingRequest{ + Request: &envoyextproc.ProcessingRequest_RequestBody{ + RequestBody: &envoyextproc.HttpBody{ Body: []byte("body"), }, }, @@ -55,12 +55,12 @@ func end2EndStreamRequest(t *testing.T, stream extproc.ExternalProcessor_Process res, err = stream.Recv() require.NoError(t, err) - require.Equal(t, extproc.CommonResponse_CONTINUE, res.GetRequestBody().GetResponse().GetStatus()) + require.Equal(t, envoyextproc.CommonResponse_CONTINUE, res.GetRequestBody().GetResponse().GetStatus()) // 3- Send the trailers - err = stream.Send(&extproc.ProcessingRequest{ - Request: &extproc.ProcessingRequest_RequestTrailers{ - RequestTrailers: &extproc.HttpTrailers{ + err = stream.Send(&envoyextproc.ProcessingRequest{ + Request: &envoyextproc.ProcessingRequest_RequestTrailers{ + RequestTrailers: &envoyextproc.HttpTrailers{ Trailers: &v3.HeaderMap{ Headers: []*v3.HeaderValue{ {Key: "key", Value: "value"}, @@ -77,9 +77,9 @@ func end2EndStreamRequest(t *testing.T, stream extproc.ExternalProcessor_Process // Second part: response // 1- Send the response headers - err = stream.Send(&extproc.ProcessingRequest{ - Request: &extproc.ProcessingRequest_ResponseHeaders{ - ResponseHeaders: &extproc.HttpHeaders{ + err = stream.Send(&envoyextproc.ProcessingRequest{ + Request: &envoyextproc.ProcessingRequest_ResponseHeaders{ + ResponseHeaders: &envoyextproc.HttpHeaders{ Headers: makeResponseHeaders(responseHeaders, "200"), }, }, @@ -94,12 +94,12 @@ func end2EndStreamRequest(t *testing.T, stream extproc.ExternalProcessor_Process res, err = stream.Recv() require.NoError(t, err) - require.Equal(t, extproc.CommonResponse_CONTINUE, res.GetResponseHeaders().GetResponse().GetStatus()) + require.Equal(t, envoyextproc.CommonResponse_CONTINUE, res.GetResponseHeaders().GetResponse().GetStatus()) // 2- Send the response body - err = stream.Send(&extproc.ProcessingRequest{ - Request: &extproc.ProcessingRequest_ResponseBody{ - ResponseBody: &extproc.HttpBody{ + err = stream.Send(&envoyextproc.ProcessingRequest{ + Request: &envoyextproc.ProcessingRequest_ResponseBody{ + ResponseBody: &envoyextproc.HttpBody{ Body: []byte("body"), EndOfStream: true, }, @@ -148,7 +148,7 @@ func TestAppSec(t *testing.T) { t.Skip("appsec disabled") } - setup := func() (extproc.ExternalProcessorClient, mocktracer.Tracer, func()) { + setup := func() (envoyextproc.ExternalProcessorClient, mocktracer.Tracer, func()) { rig, err := newEnvoyAppsecRig(false) require.NoError(t, err) @@ -187,9 +187,9 @@ func TestAppSec(t *testing.T) { stream, err := client.Process(ctx) require.NoError(t, err) - err = stream.Send(&extproc.ProcessingRequest{ - Request: &extproc.ProcessingRequest_RequestHeaders{ - RequestHeaders: &extproc.HttpHeaders{ + err = stream.Send(&envoyextproc.ProcessingRequest{ + Request: &envoyextproc.ProcessingRequest_RequestHeaders{ + RequestHeaders: &envoyextproc.HttpHeaders{ Headers: makeRequestHeaders(map[string]string{"User-Agent": "dd-test-scanner-log-block"}, "GET", "/"), }, }, @@ -198,7 +198,7 @@ func TestAppSec(t *testing.T) { res, err := stream.Recv() require.Equal(t, uint32(0), res.GetImmediateResponse().GetGrpcStatus().Status) - require.Equal(t, typev3.StatusCode(403), res.GetImmediateResponse().GetStatus().Code) + require.Equal(t, envoytypes.StatusCode(403), res.GetImmediateResponse().GetStatus().Code) require.Equal(t, "Content-Type", res.GetImmediateResponse().GetHeaders().SetHeaders[0].GetHeader().Key) require.Equal(t, "application/json", string(res.GetImmediateResponse().GetHeaders().SetHeaders[0].GetHeader().RawValue)) require.NoError(t, err) @@ -226,7 +226,7 @@ func TestBlockingWithUserRulesFile(t *testing.T) { t.Skip("appsec disabled") } - setup := func() (extproc.ExternalProcessorClient, mocktracer.Tracer, func()) { + setup := func() (envoyextproc.ExternalProcessorClient, mocktracer.Tracer, func()) { rig, err := newEnvoyAppsecRig(false) require.NoError(t, err) @@ -246,12 +246,13 @@ func TestBlockingWithUserRulesFile(t *testing.T) { stream, err := client.Process(ctx) require.NoError(t, err) - end2EndStreamRequest(t, stream, "/", "OPTION", map[string]string{"User-Agent": "dd-test-scanner-log-block"}, map[string]string{"User-Agent": "match-response-header"}, true) + end2EndStreamRequest(t, stream, "/", "OPTION", map[string]string{"User-Agent": "dd-test-scanner-log-block"}, map[string]string{"User-Agent": "match-response-headers"}, true) // Handle the immediate response res, err := stream.Recv() require.Equal(t, uint32(0), res.GetImmediateResponse().GetGrpcStatus().Status) - require.Equal(t, typev3.StatusCode(418), res.GetImmediateResponse().GetStatus().Code) // 418 because of the rule file + require.Equal(t, envoytypes.StatusCode(418), res.GetImmediateResponse().GetStatus().Code) // 418 because of the rule file + require.Len(t, res.GetImmediateResponse().GetHeaders().SetHeaders, 1) require.Equal(t, "Content-Type", res.GetImmediateResponse().GetHeaders().SetHeaders[0].GetHeader().Key) require.Equal(t, "application/json", string(res.GetImmediateResponse().GetHeaders().SetHeaders[0].GetHeader().RawValue)) require.NoError(t, err) @@ -279,9 +280,9 @@ func TestBlockingWithUserRulesFile(t *testing.T) { stream, err := client.Process(ctx) require.NoError(t, err) - err = stream.Send(&extproc.ProcessingRequest{ - Request: &extproc.ProcessingRequest_RequestHeaders{ - RequestHeaders: &extproc.HttpHeaders{ + err = stream.Send(&envoyextproc.ProcessingRequest{ + Request: &envoyextproc.ProcessingRequest_RequestHeaders{ + RequestHeaders: &envoyextproc.HttpHeaders{ Headers: makeRequestHeaders(map[string]string{"User-Agent": "Mistake Not..."}, "GET", "/hello?match=match-request-query"), }, }, @@ -290,7 +291,7 @@ func TestBlockingWithUserRulesFile(t *testing.T) { res, err := stream.Recv() require.Equal(t, uint32(0), res.GetImmediateResponse().GetGrpcStatus().Status) - require.Equal(t, typev3.StatusCode(418), res.GetImmediateResponse().GetStatus().Code) + require.Equal(t, envoytypes.StatusCode(418), res.GetImmediateResponse().GetStatus().Code) require.Equal(t, "Content-Type", res.GetImmediateResponse().GetHeaders().SetHeaders[0].GetHeader().Key) require.Equal(t, "application/json", string(res.GetImmediateResponse().GetHeaders().SetHeaders[0].GetHeader().RawValue)) require.NoError(t, err) @@ -317,9 +318,9 @@ func TestBlockingWithUserRulesFile(t *testing.T) { stream, err := client.Process(ctx) require.NoError(t, err) - err = stream.Send(&extproc.ProcessingRequest{ - Request: &extproc.ProcessingRequest_RequestHeaders{ - RequestHeaders: &extproc.HttpHeaders{ + err = stream.Send(&envoyextproc.ProcessingRequest{ + Request: &envoyextproc.ProcessingRequest_RequestHeaders{ + RequestHeaders: &envoyextproc.HttpHeaders{ Headers: makeRequestHeaders(map[string]string{"Cookie": "foo=jdfoSDGFkivRG_234"}, "OPTIONS", "/"), }, }, @@ -328,7 +329,7 @@ func TestBlockingWithUserRulesFile(t *testing.T) { res, err := stream.Recv() require.Equal(t, uint32(0), res.GetImmediateResponse().GetGrpcStatus().Status) - require.Equal(t, typev3.StatusCode(418), res.GetImmediateResponse().GetStatus().Code) + require.Equal(t, envoytypes.StatusCode(418), res.GetImmediateResponse().GetStatus().Code) require.Equal(t, "Content-Type", res.GetImmediateResponse().GetHeaders().SetHeaders[0].GetHeader().Key) require.Equal(t, "application/json", string(res.GetImmediateResponse().GetHeaders().SetHeaders[0].GetHeader().RawValue)) require.NoError(t, err) @@ -349,7 +350,7 @@ func TestBlockingWithUserRulesFile(t *testing.T) { } func TestGeneratedSpan(t *testing.T) { - setup := func() (extproc.ExternalProcessorClient, mocktracer.Tracer, func()) { + setup := func() (envoyextproc.ExternalProcessorClient, mocktracer.Tracer, func()) { rig, err := newEnvoyAppsecRig(false) require.NoError(t, err) @@ -384,7 +385,7 @@ func TestGeneratedSpan(t *testing.T) { require.Equal(t, "https://datadoghq.com/resource-span", span.Tag("http.url")) require.Equal(t, "GET", span.Tag("http.method")) require.Equal(t, "datadoghq.com", span.Tag("http.host")) - require.Equal(t, "GET /resource-span", span.Tag("resource.name")) + // require.Equal(t, "GET /resource-span", span.Tag("resource.name")) require.Equal(t, "server", span.Tag("span.kind")) require.Equal(t, "Mistake Not...", span.Tag("http.useragent")) }) @@ -398,7 +399,7 @@ func TestXForwardedForHeaderClientIp(t *testing.T) { t.Skip("appsec disabled") } - setup := func() (extproc.ExternalProcessorClient, mocktracer.Tracer, func()) { + setup := func() (envoyextproc.ExternalProcessorClient, mocktracer.Tracer, func()) { rig, err := newEnvoyAppsecRig(false) require.NoError(t, err) @@ -420,7 +421,7 @@ func TestXForwardedForHeaderClientIp(t *testing.T) { end2EndStreamRequest(t, stream, "/", "OPTION", map[string]string{"User-Agent": "Mistake not...", "X-Forwarded-For": "18.18.18.18"}, - map[string]string{"User-Agent": "match-response-header"}, + map[string]string{"User-Agent": "match-response-headers"}, true) err = stream.CloseSend() @@ -446,9 +447,9 @@ func TestXForwardedForHeaderClientIp(t *testing.T) { stream, err := client.Process(ctx) require.NoError(t, err) - err = stream.Send(&extproc.ProcessingRequest{ - Request: &extproc.ProcessingRequest_RequestHeaders{ - RequestHeaders: &extproc.HttpHeaders{ + err = stream.Send(&envoyextproc.ProcessingRequest{ + Request: &envoyextproc.ProcessingRequest_RequestHeaders{ + RequestHeaders: &envoyextproc.HttpHeaders{ Headers: makeRequestHeaders(map[string]string{"User-Agent": "Mistake not...", "X-Forwarded-For": "1.2.3.4"}, "GET", "/"), }, }, @@ -458,7 +459,7 @@ func TestXForwardedForHeaderClientIp(t *testing.T) { // Handle the immediate response res, err := stream.Recv() require.Equal(t, uint32(0), res.GetImmediateResponse().GetGrpcStatus().Status) - require.Equal(t, typev3.StatusCode(403), res.GetImmediateResponse().GetStatus().Code) + require.Equal(t, envoytypes.StatusCode(403), res.GetImmediateResponse().GetStatus().Code) require.Equal(t, "Content-Type", res.GetImmediateResponse().GetHeaders().SetHeaders[0].GetHeader().Key) require.Equal(t, "application/json", string(res.GetImmediateResponse().GetHeaders().SetHeaders[0].GetHeader().RawValue)) require.NoError(t, err) @@ -488,7 +489,7 @@ func newEnvoyAppsecRig(traceClient bool, interceptorOpts ...ddgrpc.Option) (*env ) fixtureServer := new(envoyFixtureServer) - extproc.RegisterExternalProcessorServer(server, fixtureServer) + envoyextproc.RegisterExternalProcessorServer(server, fixtureServer) li, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { @@ -514,7 +515,7 @@ func newEnvoyAppsecRig(traceClient bool, interceptorOpts ...ddgrpc.Option) (*env port: port, server: server, conn: conn, - client: extproc.NewExternalProcessorClient(conn), + client: envoyextproc.NewExternalProcessorClient(conn), }, err } @@ -525,7 +526,7 @@ type envoyAppsecRig struct { port string listener net.Listener conn *grpc.ClientConn - client extproc.ExternalProcessorClient + client envoyextproc.ExternalProcessorClient } func (r *envoyAppsecRig) Close() { @@ -534,7 +535,7 @@ func (r *envoyAppsecRig) Close() { } type envoyFixtureServer struct { - extproc.ExternalProcessorServer + envoyextproc.ExternalProcessorServer } // Helper functions diff --git a/contrib/envoyproxy/envoy/fakehttp.go b/contrib/envoyproxy/envoy/fakehttp.go new file mode 100644 index 0000000000..2d1a4652b1 --- /dev/null +++ b/contrib/envoyproxy/envoy/fakehttp.go @@ -0,0 +1,189 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2024 Datadog, Inc. + +package envoy + +import ( + "context" + "crypto/tls" + "fmt" + "net/http" + "net/url" + "strconv" + "strings" + "sync" + + corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" + extproc "github.com/envoyproxy/go-control-plane/envoy/service/ext_proc/v3" + "google.golang.org/grpc/metadata" +) + +// checkPseudoRequestHeaders Verify the required HTTP2 headers are present +// Some mandatory headers need to be set. It can happen when it wasn't a real HTTP2 request sent by Envoy, +func checkPseudoRequestHeaders(headers map[string]string) error { + for _, header := range []string{":authority", ":scheme", ":path", ":method"} { + if _, ok := headers[header]; !ok { + return fmt.Errorf("missing required headers: %q", header) + } + } + + return nil +} + +// checkPseudoResponseHeaders Verify the required HTTP2 headers are present +// Some mandatory headers need to be set. It can happen when it wasn't a real HTTP2 request sent by Envoy, +func checkPseudoResponseHeaders(headers map[string]string) error { + if _, ok := headers[":status"]; !ok { + return fmt.Errorf("missing required ':status' headers") + } + + return nil +} + +func getRemoteAddr(md metadata.MD) string { + xfwd := md.Get("x-forwarded-for") + length := len(xfwd) + if length == 0 { + return "" + } + + // Get the first right value of x-forwarded-for headers + // The rightmost IP address is the one that will be used as the remote client IP + // https://datadoghq.atlassian.net/wiki/spaces/TS/pages/2766733526/Sensitive+IP+information#Where-does-the-value-of-the-http.client_ip-tag-come-from%3F + return xfwd[length-1] +} + +// partitionPeusdoHeaders Separate normal headers of the initial request made by the client and the pseudo headers of HTTP/2 +// - Format the headers to be used by the tracer as a map[string][]string +// - Set headers keys to be canonical +func partitionPeusdoHeaders(receivedHeaders []*corev3.HeaderValue) (map[string][]string, map[string]string) { + headers := make(map[string][]string, len(receivedHeaders)-4) + pseudoHeaders := make(map[string]string, 4) + for _, v := range receivedHeaders { + key := v.GetKey() + if key == "" { + continue + } + if key[0] == ':' { + pseudoHeaders[key] = string(v.GetRawValue()) + continue + } + + headers[http.CanonicalHeaderKey(key)] = []string{string(v.GetRawValue())} + } + return headers, pseudoHeaders +} + +func NewFakeResponseWriterFromExtProc(w http.ResponseWriter, res *extproc.ProcessingRequest_ResponseHeaders) error { + headers, pseudoHeaders := partitionPeusdoHeaders(res.ResponseHeaders.GetHeaders().GetHeaders()) + + if err := checkPseudoResponseHeaders(pseudoHeaders); err != nil { + return err + } + + status, err := strconv.Atoi(pseudoHeaders[":status"]) + if err != nil { + return fmt.Errorf("error parsing status code %q: %w", pseudoHeaders[":status"], err) + } + + for k, v := range headers { + w.Header().Set(k, strings.Join(v, ",")) + } + + w.WriteHeader(status) + return nil +} + +// NewRequestFromExtProc creates a new http.Request from an ext_proc RequestHeaders message +func NewRequestFromExtProc(ctx context.Context, req *extproc.ProcessingRequest_RequestHeaders) (*http.Request, error) { + headers, pseudoHeaders := partitionPeusdoHeaders(req.RequestHeaders.GetHeaders().GetHeaders()) + if err := checkPseudoRequestHeaders(pseudoHeaders); err != nil { + return nil, err + } + + parsedURL, err := url.Parse(fmt.Sprintf("%s://%s%s", pseudoHeaders[":scheme"], pseudoHeaders[":authority"], pseudoHeaders[":path"])) + if err != nil { + return nil, fmt.Errorf( + "error building envoy URI from scheme %q, from host %q and from path %q: %w", + pseudoHeaders[":scheme"], + pseudoHeaders[":host"], + pseudoHeaders[":path"], + err) + } + + var remoteAddr string + md, ok := metadata.FromIncomingContext(ctx) + if ok { + remoteAddr = getRemoteAddr(md) + } + + var tlsState *tls.ConnectionState + if pseudoHeaders[":scheme"] == "https" { + tlsState = &tls.ConnectionState{} + } + + headers["Host"] = append(headers["Host"], pseudoHeaders[":authority"]) + + return (&http.Request{ + Method: pseudoHeaders[":method"], + Host: pseudoHeaders[":authority"], + RequestURI: pseudoHeaders[":path"], + URL: parsedURL, + Header: headers, + RemoteAddr: remoteAddr, + TLS: tlsState, + }).WithContext(ctx), nil +} + +type FakeResponseWriter struct { + mu sync.Mutex + status int + body []byte + headers http.Header +} + +// Reset resets the FakeResponseWriter to its initial state +func (w *FakeResponseWriter) Reset() { + w.mu.Lock() + defer w.mu.Unlock() + w.status = 0 + w.body = nil + w.headers = make(http.Header) +} + +// Status is not in the [http.ResponseWriter] interface, but it is cast into it by the tracing code +func (w *FakeResponseWriter) Status() int { + w.mu.Lock() + defer w.mu.Unlock() + return w.status +} + +func (w *FakeResponseWriter) WriteHeader(status int) { + w.mu.Lock() + defer w.mu.Unlock() + w.status = status +} + +func (w *FakeResponseWriter) Header() http.Header { + w.mu.Lock() + defer w.mu.Unlock() + return w.headers +} + +func (w *FakeResponseWriter) Write(b []byte) (int, error) { + w.mu.Lock() + defer w.mu.Unlock() + w.body = append(w.body, b...) + return len(b), nil +} + +var _ http.ResponseWriter = &FakeResponseWriter{} + +// NewFakeResponseWriter creates a new FakeResponseWriter that can be used to store the response a [http.Handler] made +func NewFakeResponseWriter() *FakeResponseWriter { + return &FakeResponseWriter{ + headers: make(http.Header), + } +} diff --git a/contrib/internal/httptrace/response_writer.go b/contrib/internal/httptrace/response_writer.go index 2bbc31bad7..f44fff762f 100644 --- a/contrib/internal/httptrace/response_writer.go +++ b/contrib/internal/httptrace/response_writer.go @@ -16,6 +16,13 @@ type responseWriter struct { status int } +// ResetStatusCode resets the status code of the response writer. +func ResetStatusCode(w http.ResponseWriter) { + if rw, ok := w.(*responseWriter); ok { + rw.status = 0 + } +} + func newResponseWriter(w http.ResponseWriter) *responseWriter { return &responseWriter{w, 0} } diff --git a/go.mod b/go.mod index 1c7d462f96..d30b045641 100644 --- a/go.mod +++ b/go.mod @@ -40,7 +40,7 @@ require ( github.com/elastic/go-elasticsearch/v8 v8.4.0 github.com/emicklei/go-restful v2.16.0+incompatible github.com/emicklei/go-restful/v3 v3.11.0 - github.com/envoyproxy/go-control-plane v0.12.0 + github.com/envoyproxy/go-control-plane v0.13.0 github.com/garyburd/redigo v1.6.4 github.com/gin-gonic/gin v1.9.1 github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8 @@ -104,7 +104,7 @@ require ( golang.org/x/time v0.6.0 golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 google.golang.org/api v0.192.0 - google.golang.org/grpc v1.64.1 + google.golang.org/grpc v1.65.0 google.golang.org/protobuf v1.34.2 gopkg.in/jinzhu/gorm.v1 v1.9.2 gopkg.in/olivere/elastic.v3 v3.0.75 @@ -243,6 +243,7 @@ require ( github.com/philhofer/fwd v1.1.3-0.20240612014219-fbbf4953d986 // indirect github.com/pierrec/lz4/v4 v4.1.18 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c // indirect github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect diff --git a/go.sum b/go.sum index 0d575fea1b..90223ae167 100644 --- a/go.sum +++ b/go.sum @@ -1121,8 +1121,8 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.m github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= github.com/envoyproxy/go-control-plane v0.10.3/go.mod h1:fJJn/j26vwOu972OllsvAgJJM//w9BV6Fxbg2LuVd34= -github.com/envoyproxy/go-control-plane v0.12.0 h1:4X+VP1GHd1Mhj6IB5mMeGbLCleqxjletLK6K0rbxyZI= -github.com/envoyproxy/go-control-plane v0.12.0/go.mod h1:ZBTaoJ23lqITozF0M6G4/IragXCQKCnYbmlmtHvwRG0= +github.com/envoyproxy/go-control-plane v0.13.0 h1:HzkeUz1Knt+3bK+8LG1bxOO/jzWZmdxpwC51i202les= +github.com/envoyproxy/go-control-plane v0.13.0/go.mod h1:GRaKG3dwvFoTg4nj7aXdZnvMg4d7nvT/wl9WgVXn3Q8= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo= github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w= @@ -1890,6 +1890,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= From 89ebfe632cf43a0565b21d32c7a6ab359ae96783 Mon Sep 17 00:00:00 2001 From: Flavien Darche Date: Wed, 20 Nov 2024 17:58:32 +0100 Subject: [PATCH 07/14] Apply comments --- .../{envoy => go-control-plane}/envoy.go | 28 +-- .../{envoy => go-control-plane}/envoy_test.go | 230 +++++++++--------- .../{envoy => go-control-plane}/fakehttp.go | 44 ++-- 3 files changed, 151 insertions(+), 151 deletions(-) rename contrib/envoyproxy/{envoy => go-control-plane}/envoy.go (93%) rename contrib/envoyproxy/{envoy => go-control-plane}/envoy_test.go (99%) rename contrib/envoyproxy/{envoy => go-control-plane}/fakehttp.go (73%) diff --git a/contrib/envoyproxy/envoy/envoy.go b/contrib/envoyproxy/go-control-plane/envoy.go similarity index 93% rename from contrib/envoyproxy/envoy/envoy.go rename to contrib/envoyproxy/go-control-plane/envoy.go index 6805c1b73a..2df4d7a51d 100644 --- a/contrib/envoyproxy/envoy/envoy.go +++ b/contrib/envoyproxy/go-control-plane/envoy.go @@ -3,7 +3,7 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2016 Datadog, Inc. -package envoy +package go_control_plane import ( "context" @@ -32,18 +32,18 @@ import ( envoytypes "github.com/envoyproxy/go-control-plane/envoy/type/v3" ) -const componentName = "envoyproxy/go-control-plane/envoy/service/ext_proc/envoycore" +const componentName = "envoyproxy/go-control-plane" func init() { telemetry.LoadIntegration(componentName) - tracer.MarkIntegrationImported("github.com/envoyproxy/go-control-plane/envoy/service/ext_proc/envoycore") + tracer.MarkIntegrationImported("github.com/envoyproxy/go-control-plane") } -type CurrentRequest struct { +type currentRequest struct { span tracer.Span afterHandle func() ctx context.Context - fakeResponseWriter *FakeResponseWriter + fakeResponseWriter *fakeResponseWriter wrappedResponseWriter http.ResponseWriter } @@ -58,7 +58,7 @@ func StreamServerInterceptor(opts ...grpctrace.Option) grpc.StreamServerIntercep var ( ctx = ss.Context() blocked bool - currentRequest *CurrentRequest + currentRequest *currentRequest processingRequest envoyextproc.ProcessingRequest processingResponse *envoyextproc.ProcessingResponse ) @@ -106,7 +106,7 @@ func StreamServerInterceptor(opts ...grpctrace.Option) grpc.StreamServerIntercep case *envoyextproc.ProcessingRequest_RequestHeaders: processingResponse, currentRequest, blocked, err = ProcessRequestHeaders(ctx, v) case *envoyextproc.ProcessingRequest_ResponseHeaders: - processingResponse, err = ProcessResponseHeaders(v, currentRequest) + processingResponse, err = processResponseHeaders(v, currentRequest) currentRequest = nil // Request is done, reset the current request } @@ -181,16 +181,16 @@ func envoyExternalProcessingRequestTypeAssert(req *envoyextproc.ProcessingReques } } -func ProcessRequestHeaders(ctx context.Context, req *envoyextproc.ProcessingRequest_RequestHeaders) (*envoyextproc.ProcessingResponse, *CurrentRequest, bool, error) { +func ProcessRequestHeaders(ctx context.Context, req *envoyextproc.ProcessingRequest_RequestHeaders) (*envoyextproc.ProcessingResponse, *currentRequest, bool, error) { log.Debug("external_processing: received request headers: %v\n", req.RequestHeaders) - request, err := NewRequestFromExtProc(ctx, req) + request, err := newRequest(ctx, req) if err != nil { return nil, nil, false, status.Errorf(codes.InvalidArgument, "Error processing request headers from ext_proc: %v", err) } var blocked bool - fakeResponseWriter := NewFakeResponseWriter() + fakeResponseWriter := newFakeResponseWriter() wrappedResponseWriter, request, afterHandle, blocked := httptrace.BeforeHandle(&httptrace.ServeConfig{ SpanOpts: []ddtrace.StartSpanOption{ tracer.Tag(ext.SpanKind, ext.SpanKindServer), @@ -214,7 +214,7 @@ func ProcessRequestHeaders(ctx context.Context, req *envoyextproc.ProcessingRequ return nil, nil, false, err } - return processingResponse, &CurrentRequest{ + return processingResponse, ¤tRequest{ span: span, ctx: request.Context(), fakeResponseWriter: fakeResponseWriter, @@ -257,10 +257,10 @@ func propagationRequestHeaderMutation(span ddtrace.Span) (*envoyextproc.Processi }, nil } -func ProcessResponseHeaders(res *envoyextproc.ProcessingRequest_ResponseHeaders, currentRequest *CurrentRequest) (*envoyextproc.ProcessingResponse, error) { +func processResponseHeaders(res *envoyextproc.ProcessingRequest_ResponseHeaders, currentRequest *currentRequest) (*envoyextproc.ProcessingResponse, error) { log.Debug("external_processing: received response headers: %v\n", res.ResponseHeaders) - if err := NewFakeResponseWriterFromExtProc(currentRequest.wrappedResponseWriter, res); err != nil { + if err := createFakeResponseWriter(currentRequest.wrappedResponseWriter, res); err != nil { return nil, status.Errorf(codes.InvalidArgument, "Error processing response headers from ext_proc: %v", err) } @@ -303,7 +303,7 @@ func ProcessResponseHeaders(res *envoyextproc.ProcessingRequest_ResponseHeaders, }, nil } -func doBlockResponse(writer *FakeResponseWriter) *envoyextproc.ProcessingResponse { +func doBlockResponse(writer *fakeResponseWriter) *envoyextproc.ProcessingResponse { var headersMutation []*envoycore.HeaderValueOption for k, v := range writer.headers { headersMutation = append(headersMutation, &envoycore.HeaderValueOption{ diff --git a/contrib/envoyproxy/envoy/envoy_test.go b/contrib/envoyproxy/go-control-plane/envoy_test.go similarity index 99% rename from contrib/envoyproxy/envoy/envoy_test.go rename to contrib/envoyproxy/go-control-plane/envoy_test.go index d8860670df..45e8ec740a 100644 --- a/contrib/envoyproxy/envoy/envoy_test.go +++ b/contrib/envoyproxy/go-control-plane/envoy_test.go @@ -5,7 +5,7 @@ // TODO: Blocking and Redirect action to test -package envoy +package go_control_plane import ( "context" @@ -27,120 +27,6 @@ import ( "google.golang.org/grpc" ) -func end2EndStreamRequest(t *testing.T, stream envoyextproc.ExternalProcessor_ProcessClient, path string, method string, requestHeaders map[string]string, responseHeaders map[string]string, blockOnResponse bool) { - // First part: request - // 1- Send the headers - err := stream.Send(&envoyextproc.ProcessingRequest{ - Request: &envoyextproc.ProcessingRequest_RequestHeaders{ - RequestHeaders: &envoyextproc.HttpHeaders{ - Headers: makeRequestHeaders(requestHeaders, method, path), - }, - }, - }) - require.NoError(t, err) - - res, err := stream.Recv() - require.NoError(t, err) - require.Equal(t, envoyextproc.CommonResponse_CONTINUE, res.GetRequestHeaders().GetResponse().GetStatus()) - - // 2- Send the body - err = stream.Send(&envoyextproc.ProcessingRequest{ - Request: &envoyextproc.ProcessingRequest_RequestBody{ - RequestBody: &envoyextproc.HttpBody{ - Body: []byte("body"), - }, - }, - }) - require.NoError(t, err) - - res, err = stream.Recv() - require.NoError(t, err) - require.Equal(t, envoyextproc.CommonResponse_CONTINUE, res.GetRequestBody().GetResponse().GetStatus()) - - // 3- Send the trailers - err = stream.Send(&envoyextproc.ProcessingRequest{ - Request: &envoyextproc.ProcessingRequest_RequestTrailers{ - RequestTrailers: &envoyextproc.HttpTrailers{ - Trailers: &v3.HeaderMap{ - Headers: []*v3.HeaderValue{ - {Key: "key", Value: "value"}, - }, - }, - }, - }, - }) - require.NoError(t, err) - - res, err = stream.Recv() - require.NoError(t, err) - require.NotNil(t, res.GetRequestTrailers()) - - // Second part: response - // 1- Send the response headers - err = stream.Send(&envoyextproc.ProcessingRequest{ - Request: &envoyextproc.ProcessingRequest_ResponseHeaders{ - ResponseHeaders: &envoyextproc.HttpHeaders{ - Headers: makeResponseHeaders(responseHeaders, "200"), - }, - }, - }) - require.NoError(t, err) - - if blockOnResponse { - // Should have received an immediate response for blocking - // Let the test handle the response - return - } - - res, err = stream.Recv() - require.NoError(t, err) - require.Equal(t, envoyextproc.CommonResponse_CONTINUE, res.GetResponseHeaders().GetResponse().GetStatus()) - - // 2- Send the response body - err = stream.Send(&envoyextproc.ProcessingRequest{ - Request: &envoyextproc.ProcessingRequest_ResponseBody{ - ResponseBody: &envoyextproc.HttpBody{ - Body: []byte("body"), - EndOfStream: true, - }, - }, - }) - require.NoError(t, err) - - // The stream should now be closed - _, err = stream.Recv() - require.Equal(t, io.EOF, err) -} - -func checkForAppsecEvent(t *testing.T, finished []mocktracer.Span, expectedRuleIDs map[string]int) { - // The request should have the attack attempts - event := finished[len(finished)-1].Tag("_dd.appsec.json") - require.NotNil(t, event, "the _dd.appsec.json tag was not found") - - jsonText := event.(string) - type trigger struct { - Rule struct { - ID string `json:"id"` - } `json:"rule"` - } - var parsed struct { - Triggers []trigger `json:"triggers"` - } - err := json.Unmarshal([]byte(jsonText), &parsed) - require.NoError(t, err) - - histogram := map[string]uint8{} - for _, tr := range parsed.Triggers { - histogram[tr.Rule.ID]++ - } - - for ruleID, count := range expectedRuleIDs { - require.Equal(t, count, int(histogram[ruleID]), "rule %s has been triggered %d times but expected %d") - } - - require.Len(t, parsed.Triggers, len(expectedRuleIDs), "unexpected number of rules triggered") -} - func TestAppSec(t *testing.T) { appsec.Start() defer appsec.Stop() @@ -540,6 +426,120 @@ type envoyFixtureServer struct { // Helper functions +func end2EndStreamRequest(t *testing.T, stream envoyextproc.ExternalProcessor_ProcessClient, path string, method string, requestHeaders map[string]string, responseHeaders map[string]string, blockOnResponse bool) { + // First part: request + // 1- Send the headers + err := stream.Send(&envoyextproc.ProcessingRequest{ + Request: &envoyextproc.ProcessingRequest_RequestHeaders{ + RequestHeaders: &envoyextproc.HttpHeaders{ + Headers: makeRequestHeaders(requestHeaders, method, path), + }, + }, + }) + require.NoError(t, err) + + res, err := stream.Recv() + require.NoError(t, err) + require.Equal(t, envoyextproc.CommonResponse_CONTINUE, res.GetRequestHeaders().GetResponse().GetStatus()) + + // 2- Send the body + err = stream.Send(&envoyextproc.ProcessingRequest{ + Request: &envoyextproc.ProcessingRequest_RequestBody{ + RequestBody: &envoyextproc.HttpBody{ + Body: []byte("body"), + }, + }, + }) + require.NoError(t, err) + + res, err = stream.Recv() + require.NoError(t, err) + require.Equal(t, envoyextproc.CommonResponse_CONTINUE, res.GetRequestBody().GetResponse().GetStatus()) + + // 3- Send the trailers + err = stream.Send(&envoyextproc.ProcessingRequest{ + Request: &envoyextproc.ProcessingRequest_RequestTrailers{ + RequestTrailers: &envoyextproc.HttpTrailers{ + Trailers: &v3.HeaderMap{ + Headers: []*v3.HeaderValue{ + {Key: "key", Value: "value"}, + }, + }, + }, + }, + }) + require.NoError(t, err) + + res, err = stream.Recv() + require.NoError(t, err) + require.NotNil(t, res.GetRequestTrailers()) + + // Second part: response + // 1- Send the response headers + err = stream.Send(&envoyextproc.ProcessingRequest{ + Request: &envoyextproc.ProcessingRequest_ResponseHeaders{ + ResponseHeaders: &envoyextproc.HttpHeaders{ + Headers: makeResponseHeaders(responseHeaders, "200"), + }, + }, + }) + require.NoError(t, err) + + if blockOnResponse { + // Should have received an immediate response for blocking + // Let the test handle the response + return + } + + res, err = stream.Recv() + require.NoError(t, err) + require.Equal(t, envoyextproc.CommonResponse_CONTINUE, res.GetResponseHeaders().GetResponse().GetStatus()) + + // 2- Send the response body + err = stream.Send(&envoyextproc.ProcessingRequest{ + Request: &envoyextproc.ProcessingRequest_ResponseBody{ + ResponseBody: &envoyextproc.HttpBody{ + Body: []byte("body"), + EndOfStream: true, + }, + }, + }) + require.NoError(t, err) + + // The stream should now be closed + _, err = stream.Recv() + require.Equal(t, io.EOF, err) +} + +func checkForAppsecEvent(t *testing.T, finished []mocktracer.Span, expectedRuleIDs map[string]int) { + // The request should have the attack attempts + event := finished[len(finished)-1].Tag("_dd.appsec.json") + require.NotNil(t, event, "the _dd.appsec.json tag was not found") + + jsonText := event.(string) + type trigger struct { + Rule struct { + ID string `json:"id"` + } `json:"rule"` + } + var parsed struct { + Triggers []trigger `json:"triggers"` + } + err := json.Unmarshal([]byte(jsonText), &parsed) + require.NoError(t, err) + + histogram := map[string]uint8{} + for _, tr := range parsed.Triggers { + histogram[tr.Rule.ID]++ + } + + for ruleID, count := range expectedRuleIDs { + require.Equal(t, count, int(histogram[ruleID]), "rule %s has been triggered %d times but expected %d") + } + + require.Len(t, parsed.Triggers, len(expectedRuleIDs), "unexpected number of rules triggered") +} + // Construct request headers func makeRequestHeaders(headers map[string]string, method string, path string) *v3.HeaderMap { h := &v3.HeaderMap{} diff --git a/contrib/envoyproxy/envoy/fakehttp.go b/contrib/envoyproxy/go-control-plane/fakehttp.go similarity index 73% rename from contrib/envoyproxy/envoy/fakehttp.go rename to contrib/envoyproxy/go-control-plane/fakehttp.go index 2d1a4652b1..3f20725e1b 100644 --- a/contrib/envoyproxy/envoy/fakehttp.go +++ b/contrib/envoyproxy/go-control-plane/fakehttp.go @@ -3,7 +3,7 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2024 Datadog, Inc. -package envoy +package go_control_plane import ( "context" @@ -32,7 +32,7 @@ func checkPseudoRequestHeaders(headers map[string]string) error { return nil } -// checkPseudoResponseHeaders Verify the required HTTP2 headers are present +// checkPseudoResponseHeaders verifies the required HTTP2 headers are present // Some mandatory headers need to be set. It can happen when it wasn't a real HTTP2 request sent by Envoy, func checkPseudoResponseHeaders(headers map[string]string) error { if _, ok := headers[":status"]; !ok { @@ -55,12 +55,12 @@ func getRemoteAddr(md metadata.MD) string { return xfwd[length-1] } -// partitionPeusdoHeaders Separate normal headers of the initial request made by the client and the pseudo headers of HTTP/2 +// splitPseudoHeaders splits normal headers of the initial request made by the client and the pseudo headers of HTTP/2 // - Format the headers to be used by the tracer as a map[string][]string // - Set headers keys to be canonical -func partitionPeusdoHeaders(receivedHeaders []*corev3.HeaderValue) (map[string][]string, map[string]string) { - headers := make(map[string][]string, len(receivedHeaders)-4) - pseudoHeaders := make(map[string]string, 4) +func splitPseudoHeaders(receivedHeaders []*corev3.HeaderValue) (headers map[string][]string, pseudoHeaders map[string]string) { + headers = make(map[string][]string, len(receivedHeaders)-4) + pseudoHeaders = make(map[string]string, 4) for _, v := range receivedHeaders { key := v.GetKey() if key == "" { @@ -76,8 +76,8 @@ func partitionPeusdoHeaders(receivedHeaders []*corev3.HeaderValue) (map[string][ return headers, pseudoHeaders } -func NewFakeResponseWriterFromExtProc(w http.ResponseWriter, res *extproc.ProcessingRequest_ResponseHeaders) error { - headers, pseudoHeaders := partitionPeusdoHeaders(res.ResponseHeaders.GetHeaders().GetHeaders()) +func createFakeResponseWriter(w http.ResponseWriter, res *extproc.ProcessingRequest_ResponseHeaders) error { + headers, pseudoHeaders := splitPseudoHeaders(res.ResponseHeaders.GetHeaders().GetHeaders()) if err := checkPseudoResponseHeaders(pseudoHeaders); err != nil { return err @@ -96,9 +96,9 @@ func NewFakeResponseWriterFromExtProc(w http.ResponseWriter, res *extproc.Proces return nil } -// NewRequestFromExtProc creates a new http.Request from an ext_proc RequestHeaders message -func NewRequestFromExtProc(ctx context.Context, req *extproc.ProcessingRequest_RequestHeaders) (*http.Request, error) { - headers, pseudoHeaders := partitionPeusdoHeaders(req.RequestHeaders.GetHeaders().GetHeaders()) +// newRequest creates a new http.Request from an ext_proc RequestHeaders message +func newRequest(ctx context.Context, req *extproc.ProcessingRequest_RequestHeaders) (*http.Request, error) { + headers, pseudoHeaders := splitPseudoHeaders(req.RequestHeaders.GetHeaders().GetHeaders()) if err := checkPseudoRequestHeaders(pseudoHeaders); err != nil { return nil, err } @@ -137,15 +137,15 @@ func NewRequestFromExtProc(ctx context.Context, req *extproc.ProcessingRequest_R }).WithContext(ctx), nil } -type FakeResponseWriter struct { +type fakeResponseWriter struct { mu sync.Mutex status int body []byte headers http.Header } -// Reset resets the FakeResponseWriter to its initial state -func (w *FakeResponseWriter) Reset() { +// Reset resets the fakeResponseWriter to its initial state +func (w *fakeResponseWriter) Reset() { w.mu.Lock() defer w.mu.Unlock() w.status = 0 @@ -154,36 +154,36 @@ func (w *FakeResponseWriter) Reset() { } // Status is not in the [http.ResponseWriter] interface, but it is cast into it by the tracing code -func (w *FakeResponseWriter) Status() int { +func (w *fakeResponseWriter) Status() int { w.mu.Lock() defer w.mu.Unlock() return w.status } -func (w *FakeResponseWriter) WriteHeader(status int) { +func (w *fakeResponseWriter) WriteHeader(status int) { w.mu.Lock() defer w.mu.Unlock() w.status = status } -func (w *FakeResponseWriter) Header() http.Header { +func (w *fakeResponseWriter) Header() http.Header { w.mu.Lock() defer w.mu.Unlock() return w.headers } -func (w *FakeResponseWriter) Write(b []byte) (int, error) { +func (w *fakeResponseWriter) Write(b []byte) (int, error) { w.mu.Lock() defer w.mu.Unlock() w.body = append(w.body, b...) return len(b), nil } -var _ http.ResponseWriter = &FakeResponseWriter{} +var _ http.ResponseWriter = &fakeResponseWriter{} -// NewFakeResponseWriter creates a new FakeResponseWriter that can be used to store the response a [http.Handler] made -func NewFakeResponseWriter() *FakeResponseWriter { - return &FakeResponseWriter{ +// newFakeResponseWriter creates a new fakeResponseWriter that can be used to store the response a [http.Handler] made +func newFakeResponseWriter() *fakeResponseWriter { + return &fakeResponseWriter{ headers: make(http.Header), } } From 24bb334d46d242ceb8c1cbef566a5ebdff7fd363 Mon Sep 17 00:00:00 2001 From: Flavien Darche Date: Wed, 20 Nov 2024 18:06:57 +0100 Subject: [PATCH 08/14] Add "t.Helper" to helper methods --- .../envoyproxy/go-control-plane/envoy_test.go | 36 ++++++++++++------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/contrib/envoyproxy/go-control-plane/envoy_test.go b/contrib/envoyproxy/go-control-plane/envoy_test.go index 45e8ec740a..c2bf8769c1 100644 --- a/contrib/envoyproxy/go-control-plane/envoy_test.go +++ b/contrib/envoyproxy/go-control-plane/envoy_test.go @@ -35,7 +35,7 @@ func TestAppSec(t *testing.T) { } setup := func() (envoyextproc.ExternalProcessorClient, mocktracer.Tracer, func()) { - rig, err := newEnvoyAppsecRig(false) + rig, err := newEnvoyAppsecRig(t, false) require.NoError(t, err) mt := mocktracer.Start() @@ -76,7 +76,7 @@ func TestAppSec(t *testing.T) { err = stream.Send(&envoyextproc.ProcessingRequest{ Request: &envoyextproc.ProcessingRequest_RequestHeaders{ RequestHeaders: &envoyextproc.HttpHeaders{ - Headers: makeRequestHeaders(map[string]string{"User-Agent": "dd-test-scanner-log-block"}, "GET", "/"), + Headers: makeRequestHeaders(t, map[string]string{"User-Agent": "dd-test-scanner-log-block"}, "GET", "/"), }, }, }) @@ -113,7 +113,7 @@ func TestBlockingWithUserRulesFile(t *testing.T) { } setup := func() (envoyextproc.ExternalProcessorClient, mocktracer.Tracer, func()) { - rig, err := newEnvoyAppsecRig(false) + rig, err := newEnvoyAppsecRig(t, false) require.NoError(t, err) mt := mocktracer.Start() @@ -169,7 +169,7 @@ func TestBlockingWithUserRulesFile(t *testing.T) { err = stream.Send(&envoyextproc.ProcessingRequest{ Request: &envoyextproc.ProcessingRequest_RequestHeaders{ RequestHeaders: &envoyextproc.HttpHeaders{ - Headers: makeRequestHeaders(map[string]string{"User-Agent": "Mistake Not..."}, "GET", "/hello?match=match-request-query"), + Headers: makeRequestHeaders(t, map[string]string{"User-Agent": "Mistake Not..."}, "GET", "/hello?match=match-request-query"), }, }, }) @@ -207,7 +207,7 @@ func TestBlockingWithUserRulesFile(t *testing.T) { err = stream.Send(&envoyextproc.ProcessingRequest{ Request: &envoyextproc.ProcessingRequest_RequestHeaders{ RequestHeaders: &envoyextproc.HttpHeaders{ - Headers: makeRequestHeaders(map[string]string{"Cookie": "foo=jdfoSDGFkivRG_234"}, "OPTIONS", "/"), + Headers: makeRequestHeaders(t, map[string]string{"Cookie": "foo=jdfoSDGFkivRG_234"}, "OPTIONS", "/"), }, }, }) @@ -237,7 +237,7 @@ func TestBlockingWithUserRulesFile(t *testing.T) { func TestGeneratedSpan(t *testing.T) { setup := func() (envoyextproc.ExternalProcessorClient, mocktracer.Tracer, func()) { - rig, err := newEnvoyAppsecRig(false) + rig, err := newEnvoyAppsecRig(t, false) require.NoError(t, err) mt := mocktracer.Start() @@ -286,7 +286,7 @@ func TestXForwardedForHeaderClientIp(t *testing.T) { } setup := func() (envoyextproc.ExternalProcessorClient, mocktracer.Tracer, func()) { - rig, err := newEnvoyAppsecRig(false) + rig, err := newEnvoyAppsecRig(t, false) require.NoError(t, err) mt := mocktracer.Start() @@ -336,7 +336,7 @@ func TestXForwardedForHeaderClientIp(t *testing.T) { err = stream.Send(&envoyextproc.ProcessingRequest{ Request: &envoyextproc.ProcessingRequest_RequestHeaders{ RequestHeaders: &envoyextproc.HttpHeaders{ - Headers: makeRequestHeaders(map[string]string{"User-Agent": "Mistake not...", "X-Forwarded-For": "1.2.3.4"}, "GET", "/"), + Headers: makeRequestHeaders(t, map[string]string{"User-Agent": "Mistake not...", "X-Forwarded-For": "1.2.3.4"}, "GET", "/"), }, }, }) @@ -367,7 +367,9 @@ func TestXForwardedForHeaderClientIp(t *testing.T) { }) } -func newEnvoyAppsecRig(traceClient bool, interceptorOpts ...ddgrpc.Option) (*envoyAppsecRig, error) { +func newEnvoyAppsecRig(t *testing.T, traceClient bool, interceptorOpts ...ddgrpc.Option) (*envoyAppsecRig, error) { + t.Helper() + interceptorOpts = append([]ddgrpc.InterceptorOption{ddgrpc.WithServiceName("grpc")}, interceptorOpts...) server := grpc.NewServer( @@ -427,12 +429,14 @@ type envoyFixtureServer struct { // Helper functions func end2EndStreamRequest(t *testing.T, stream envoyextproc.ExternalProcessor_ProcessClient, path string, method string, requestHeaders map[string]string, responseHeaders map[string]string, blockOnResponse bool) { + t.Helper() + // First part: request // 1- Send the headers err := stream.Send(&envoyextproc.ProcessingRequest{ Request: &envoyextproc.ProcessingRequest_RequestHeaders{ RequestHeaders: &envoyextproc.HttpHeaders{ - Headers: makeRequestHeaders(requestHeaders, method, path), + Headers: makeRequestHeaders(t, requestHeaders, method, path), }, }, }) @@ -479,7 +483,7 @@ func end2EndStreamRequest(t *testing.T, stream envoyextproc.ExternalProcessor_Pr err = stream.Send(&envoyextproc.ProcessingRequest{ Request: &envoyextproc.ProcessingRequest_ResponseHeaders{ ResponseHeaders: &envoyextproc.HttpHeaders{ - Headers: makeResponseHeaders(responseHeaders, "200"), + Headers: makeResponseHeaders(t, responseHeaders, "200"), }, }, }) @@ -512,6 +516,8 @@ func end2EndStreamRequest(t *testing.T, stream envoyextproc.ExternalProcessor_Pr } func checkForAppsecEvent(t *testing.T, finished []mocktracer.Span, expectedRuleIDs map[string]int) { + t.Helper() + // The request should have the attack attempts event := finished[len(finished)-1].Tag("_dd.appsec.json") require.NotNil(t, event, "the _dd.appsec.json tag was not found") @@ -541,7 +547,9 @@ func checkForAppsecEvent(t *testing.T, finished []mocktracer.Span, expectedRuleI } // Construct request headers -func makeRequestHeaders(headers map[string]string, method string, path string) *v3.HeaderMap { +func makeRequestHeaders(t *testing.T, headers map[string]string, method string, path string) *v3.HeaderMap { + t.Helper() + h := &v3.HeaderMap{} for k, v := range headers { h.Headers = append(h.Headers, &v3.HeaderValue{Key: k, RawValue: []byte(v)}) @@ -557,7 +565,9 @@ func makeRequestHeaders(headers map[string]string, method string, path string) * return h } -func makeResponseHeaders(headers map[string]string, status string) *v3.HeaderMap { +func makeResponseHeaders(t *testing.T, headers map[string]string, status string) *v3.HeaderMap { + t.Helper() + h := &v3.HeaderMap{} for k, v := range headers { h.Headers = append(h.Headers, &v3.HeaderValue{Key: k, RawValue: []byte(v)}) From 46a08b12aa546fd260513ca49ead9fbb07be6786 Mon Sep 17 00:00:00 2001 From: Flavien Darche Date: Thu, 28 Nov 2024 15:44:55 +0100 Subject: [PATCH 09/14] Downgrade grpc version to v1.64.0 (some proto changes) --- contrib/envoyproxy/go-control-plane/envoy.go | 4 ++-- go.mod | 5 ++--- go.sum | 6 ++---- internal/apps/go.mod | 2 +- internal/apps/go.sum | 4 ++-- internal/exectracetest/go.mod | 2 +- internal/exectracetest/go.sum | 4 ++-- 7 files changed, 12 insertions(+), 15 deletions(-) diff --git a/contrib/envoyproxy/go-control-plane/envoy.go b/contrib/envoyproxy/go-control-plane/envoy.go index 2df4d7a51d..747b7ea26f 100644 --- a/contrib/envoyproxy/go-control-plane/envoy.go +++ b/contrib/envoyproxy/go-control-plane/envoy.go @@ -51,7 +51,7 @@ func StreamServerInterceptor(opts ...grpctrace.Option) grpc.StreamServerIntercep interceptor := grpctrace.StreamServerInterceptor(opts...) return func(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { - if info.FullMethod != envoyextproc.ExternalProcessor_Process_FullMethodName { + if info.FullMethod != "/envoy.service.ext_proc.v3.ExternalProcessor/Process" { return interceptor(srv, ss, info, handler) } @@ -328,7 +328,7 @@ func doBlockResponse(writer *fakeResponseWriter) *envoyextproc.ProcessingRespons Headers: &envoyextproc.HeaderMutation{ SetHeaders: headersMutation, }, - Body: writer.body, + Body: string(writer.body), GrpcStatus: &envoyextproc.GrpcStatus{ Status: 0, }, diff --git a/go.mod b/go.mod index d30b045641..1c7d462f96 100644 --- a/go.mod +++ b/go.mod @@ -40,7 +40,7 @@ require ( github.com/elastic/go-elasticsearch/v8 v8.4.0 github.com/emicklei/go-restful v2.16.0+incompatible github.com/emicklei/go-restful/v3 v3.11.0 - github.com/envoyproxy/go-control-plane v0.13.0 + github.com/envoyproxy/go-control-plane v0.12.0 github.com/garyburd/redigo v1.6.4 github.com/gin-gonic/gin v1.9.1 github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8 @@ -104,7 +104,7 @@ require ( golang.org/x/time v0.6.0 golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 google.golang.org/api v0.192.0 - google.golang.org/grpc v1.65.0 + google.golang.org/grpc v1.64.1 google.golang.org/protobuf v1.34.2 gopkg.in/jinzhu/gorm.v1 v1.9.2 gopkg.in/olivere/elastic.v3 v3.0.75 @@ -243,7 +243,6 @@ require ( github.com/philhofer/fwd v1.1.3-0.20240612014219-fbbf4953d986 // indirect github.com/pierrec/lz4/v4 v4.1.18 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c // indirect github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect diff --git a/go.sum b/go.sum index 90223ae167..0d575fea1b 100644 --- a/go.sum +++ b/go.sum @@ -1121,8 +1121,8 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.m github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= github.com/envoyproxy/go-control-plane v0.10.3/go.mod h1:fJJn/j26vwOu972OllsvAgJJM//w9BV6Fxbg2LuVd34= -github.com/envoyproxy/go-control-plane v0.13.0 h1:HzkeUz1Knt+3bK+8LG1bxOO/jzWZmdxpwC51i202les= -github.com/envoyproxy/go-control-plane v0.13.0/go.mod h1:GRaKG3dwvFoTg4nj7aXdZnvMg4d7nvT/wl9WgVXn3Q8= +github.com/envoyproxy/go-control-plane v0.12.0 h1:4X+VP1GHd1Mhj6IB5mMeGbLCleqxjletLK6K0rbxyZI= +github.com/envoyproxy/go-control-plane v0.12.0/go.mod h1:ZBTaoJ23lqITozF0M6G4/IragXCQKCnYbmlmtHvwRG0= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo= github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w= @@ -1890,8 +1890,6 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= -github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= -github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= diff --git a/internal/apps/go.mod b/internal/apps/go.mod index 4a3917bb42..9ce5485a85 100644 --- a/internal/apps/go.mod +++ b/internal/apps/go.mod @@ -56,7 +56,7 @@ require ( golang.org/x/text v0.17.0 // indirect golang.org/x/tools v0.24.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240730163845-b1a4ccb954bf // indirect - google.golang.org/grpc v1.65.0 // indirect + google.golang.org/grpc v1.64.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/internal/apps/go.sum b/internal/apps/go.sum index 4aec2858b9..a591990f7b 100644 --- a/internal/apps/go.sum +++ b/internal/apps/go.sum @@ -282,8 +282,8 @@ google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAs google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto/googleapis/rpc v0.0.0-20240730163845-b1a4ccb954bf h1:liao9UHurZLtiEwBgT9LMOnKYsHze6eA6w1KQCMVN2Q= google.golang.org/genproto/googleapis/rpc v0.0.0-20240730163845-b1a4ccb954bf/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= -google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= -google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= +google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA= +google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= diff --git a/internal/exectracetest/go.mod b/internal/exectracetest/go.mod index 2efa62f766..0bc1db3393 100644 --- a/internal/exectracetest/go.mod +++ b/internal/exectracetest/go.mod @@ -73,7 +73,7 @@ require ( golang.org/x/tools v0.24.0 // indirect golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240730163845-b1a4ccb954bf // indirect - google.golang.org/grpc v1.65.0 // indirect + google.golang.org/grpc v1.64.1 // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/internal/exectracetest/go.sum b/internal/exectracetest/go.sum index 0e710320f7..0d8bd345c4 100644 --- a/internal/exectracetest/go.sum +++ b/internal/exectracetest/go.sum @@ -290,8 +290,8 @@ google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAs google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto/googleapis/rpc v0.0.0-20240730163845-b1a4ccb954bf h1:liao9UHurZLtiEwBgT9LMOnKYsHze6eA6w1KQCMVN2Q= google.golang.org/genproto/googleapis/rpc v0.0.0-20240730163845-b1a4ccb954bf/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= -google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= -google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= +google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA= +google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From c789009d744e18604b3b159da7002ce69582c2ed Mon Sep 17 00:00:00 2001 From: Flavien Darche Date: Thu, 28 Nov 2024 15:57:46 +0100 Subject: [PATCH 10/14] Add example_test --- .../go-control-plane/example_test.go | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 contrib/envoyproxy/go-control-plane/example_test.go diff --git a/contrib/envoyproxy/go-control-plane/example_test.go b/contrib/envoyproxy/go-control-plane/example_test.go new file mode 100644 index 0000000000..d9837d3091 --- /dev/null +++ b/contrib/envoyproxy/go-control-plane/example_test.go @@ -0,0 +1,34 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2024 Datadog, Inc. + +package go_control_plane_test + +import ( + "google.golang.org/grpc" + "gopkg.in/DataDog/dd-trace-go.v1/contrib/envoyproxy/go-control-plane" + "log" + "net" +) + +func Example_server() { + // 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 + + // Start serving incoming connections. + if err := s.Serve(ln); err != nil { + log.Fatalf("failed to serve: %v", err) + } +} From 961d73d441654417ca55a83606e92f0424c3d9d5 Mon Sep 17 00:00:00 2001 From: Flavien Darche Date: Thu, 28 Nov 2024 15:58:31 +0100 Subject: [PATCH 11/14] update some comments --- contrib/envoyproxy/go-control-plane/envoy.go | 2 +- contrib/envoyproxy/go-control-plane/envoy_test.go | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/contrib/envoyproxy/go-control-plane/envoy.go b/contrib/envoyproxy/go-control-plane/envoy.go index 747b7ea26f..853b4657bf 100644 --- a/contrib/envoyproxy/go-control-plane/envoy.go +++ b/contrib/envoyproxy/go-control-plane/envoy.go @@ -1,7 +1,7 @@ // Unless explicitly stated otherwise all files in this repository are licensed // under the Apache License Version 2.0. // This product includes software developed at Datadog (https://www.datadoghq.com/). -// Copyright 2016 Datadog, Inc. +// Copyright 2024 Datadog, Inc. package go_control_plane diff --git a/contrib/envoyproxy/go-control-plane/envoy_test.go b/contrib/envoyproxy/go-control-plane/envoy_test.go index c2bf8769c1..9393278958 100644 --- a/contrib/envoyproxy/go-control-plane/envoy_test.go +++ b/contrib/envoyproxy/go-control-plane/envoy_test.go @@ -1,9 +1,7 @@ // Unless explicitly stated otherwise all files in this repository are licensed // under the Apache License Version 2.0. // This product includes software developed at Datadog (https://www.datadoghq.com/). -// Copyright 2016 Datadog, Inc. - -// TODO: Blocking and Redirect action to test +// Copyright 2024 Datadog, Inc. package go_control_plane From ee0cd57697266ba9bd015acea0a5d140d0beb144 Mon Sep 17 00:00:00 2001 From: Flavien Darche Date: Wed, 11 Dec 2024 11:19:34 +0100 Subject: [PATCH 12/14] go mod tidy (rebase fix) --- go.mod | 14 +++++++------- go.sum | 8 ++------ 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/go.mod b/go.mod index 1c7d462f96..ae30ca75d1 100644 --- a/go.mod +++ b/go.mod @@ -99,12 +99,12 @@ require ( go.opentelemetry.io/otel/trace v1.27.0 go.uber.org/goleak v1.3.0 golang.org/x/mod v0.20.0 - golang.org/x/oauth2 v0.20.0 + golang.org/x/oauth2 v0.18.0 golang.org/x/sys v0.24.0 golang.org/x/time v0.6.0 golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 - google.golang.org/api v0.192.0 - google.golang.org/grpc v1.64.1 + google.golang.org/api v0.169.0 + google.golang.org/grpc v1.64.0 google.golang.org/protobuf v1.34.2 gopkg.in/jinzhu/gorm.v1 v1.9.2 gopkg.in/olivere/elastic.v3 v3.0.75 @@ -121,7 +121,7 @@ require ( require ( cloud.google.com/go v0.112.1 // indirect cloud.google.com/go/compute v1.25.1 // indirect - cloud.google.com/go/compute/metadata v0.3.0 // indirect + cloud.google.com/go/compute/metadata v0.2.3 // indirect cloud.google.com/go/iam v1.1.6 // indirect github.com/DataDog/datadog-agent/pkg/util/log v0.58.0 // indirect github.com/DataDog/datadog-agent/pkg/util/scrubber v0.58.0 // indirect @@ -155,7 +155,7 @@ require ( github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect github.com/chenzhuoyu/iasm v0.9.0 // indirect github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575 // indirect - github.com/cncf/xds/go v0.0.0-20240423153145-555b57ec207b // indirect + github.com/cncf/xds/go v0.0.0-20240318125728-8a4994d93e50 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dustin/go-humanize v1.0.1 // indirect @@ -300,8 +300,8 @@ require ( golang.org/x/tools v0.24.0 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240520151616-dc85e6b867a5 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index 0d575fea1b..5c4083713c 100644 --- a/go.sum +++ b/go.sum @@ -177,7 +177,6 @@ cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1h cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM= cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= -cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= cloud.google.com/go/contactcenterinsights v1.3.0/go.mod h1:Eu2oemoePuEFc/xKFPjbTuPSj0fYJcPls9TFlPNnHHY= cloud.google.com/go/contactcenterinsights v1.4.0/go.mod h1:L2YzkGbPsv+vMQMCADxJoT9YiTTnSEd6fEvCeHTYVck= cloud.google.com/go/contactcenterinsights v1.6.0/go.mod h1:IIDlT6CLcDoyv79kDv8iWxMSTZhLxSCofVV5W6YFM/w= @@ -891,8 +890,8 @@ github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20240423153145-555b57ec207b h1:ga8SEFjZ60pxLcmhnThWgvH2wg8376yUJmPhEH4H3kw= -github.com/cncf/xds/go v0.0.0-20240423153145-555b57ec207b/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/cncf/xds/go v0.0.0-20240318125728-8a4994d93e50 h1:DBmgJDC9dTfkVyGgipamEh2BpGYxScCH1TOF1LL1cXc= +github.com/cncf/xds/go v0.0.0-20240318125728-8a4994d93e50/go.mod h1:5e1+Vvlzido69INQaVO6d87Qn543Xr6nooe9Kz7oBFM= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5/go.mod h1:h6jFvWxBdQXxjopDMZyH2UVceIRfR84bdzbkoKrsWNo= github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoCr5oaCLELYA= @@ -2490,7 +2489,6 @@ golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI= golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8= -golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -3015,10 +3013,8 @@ google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 h1:9+tzLLstTlPTRyJ google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:mqHbVIp48Muh7Ywss/AD6I5kNVKZMmAa/QEW58Gxp2s= google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 h1:RFiFrvy37/mpSpdySBDrUdipW/dHwsRwh3J3+A9VgT4= google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237/go.mod h1:Z5Iiy3jtmioajWHDGFk7CeugTyHtPvMHA4UTmUkyalE= -google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157/go.mod h1:99sLkeliLXfdj2J75X3Ho+rrVCaJze0uwN7zDDkjPVU= google.golang.org/genproto/googleapis/rpc v0.0.0-20240520151616-dc85e6b867a5 h1:Q2RxlXqh1cgzzUgV261vBO2jI5R/3DD1J2pM0nI4NhU= google.golang.org/genproto/googleapis/rpc v0.0.0-20240520151616-dc85e6b867a5/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= From 22d70957dd57ffcc9a2c354d20d55d1993ed12e0 Mon Sep 17 00:00:00 2001 From: Flavien Darche Date: Wed, 11 Dec 2024 14:54:23 +0100 Subject: [PATCH 13/14] Applied comments --- contrib/envoyproxy/go-control-plane/envoy.go | 163 +++++++++--------- .../envoyproxy/go-control-plane/envoy_test.go | 7 +- .../go-control-plane/example_test.go | 22 ++- 3 files changed, 105 insertions(+), 87 deletions(-) diff --git a/contrib/envoyproxy/go-control-plane/envoy.go b/contrib/envoyproxy/go-control-plane/envoy.go index 853b4657bf..80a20ab0e5 100644 --- a/contrib/envoyproxy/go-control-plane/envoy.go +++ b/contrib/envoyproxy/go-control-plane/envoy.go @@ -13,7 +13,6 @@ import ( "net/http" "strings" - grpctrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/google.golang.org/grpc" "gopkg.in/DataDog/dd-trace-go.v1/contrib/internal/httptrace" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext" @@ -23,7 +22,6 @@ import ( "gopkg.in/DataDog/dd-trace-go.v1/internal/log" "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry" - "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -39,6 +37,16 @@ func init() { tracer.MarkIntegrationImported("github.com/envoyproxy/go-control-plane") } +// appsecEnvoyExternalProcessorServer is a server that implements the Envoy ExternalProcessorServer interface. +type appsecEnvoyExternalProcessorServer struct { + envoyextproc.ExternalProcessorServer +} + +// AppsecEnvoyExternalProcessorServer creates and returns a new instance of appsecEnvoyExternalProcessorServer. +func AppsecEnvoyExternalProcessorServer(userImplementation envoyextproc.ExternalProcessorServer) envoyextproc.ExternalProcessorServer { + return &appsecEnvoyExternalProcessorServer{userImplementation} +} + type currentRequest struct { span tracer.Span afterHandle func() @@ -47,96 +55,99 @@ type currentRequest struct { wrappedResponseWriter http.ResponseWriter } -func StreamServerInterceptor(opts ...grpctrace.Option) grpc.StreamServerInterceptor { - interceptor := grpctrace.StreamServerInterceptor(opts...) - - return func(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { - if info.FullMethod != "/envoy.service.ext_proc.v3.ExternalProcessor/Process" { - return interceptor(srv, ss, info, handler) +// Process handles the bidirectional stream that Envoy uses to give the server control +// over what the filter does. It processes incoming requests and sends appropriate responses +// based on the type of request received. +// +// The method receive incoming requests, processes them, and sends responses back to the client. +// It handles different types of requests such as request headers, response headers, request body, +// response body, request trailers, and response trailers. +// +// If the request is blocked, it sends an immediate response and ends the stream. If an error occurs +// during processing, it logs the error and returns an appropriate gRPC status error. +func (s *appsecEnvoyExternalProcessorServer) Process(processServer envoyextproc.ExternalProcessor_ProcessServer) error { + + var ( + ctx = processServer.Context() + blocked bool + currentRequest *currentRequest + processingRequest envoyextproc.ProcessingRequest + processingResponse *envoyextproc.ProcessingResponse + ) + + // Close the span when the request is done processing + defer func() { + if currentRequest != nil { + log.Warn("external_processing: stream stopped during a request, making sure the current span is closed\n") + currentRequest.span.Finish() + currentRequest = nil } + }() - var ( - ctx = ss.Context() - blocked bool - currentRequest *currentRequest - processingRequest envoyextproc.ProcessingRequest - processingResponse *envoyextproc.ProcessingResponse - ) - - // Close the span when the request is done processing - defer func() { - if currentRequest != nil { - log.Warn("external_processing: stream stopped during a request, making sure the current span is closed\n") - currentRequest.span.Finish() - currentRequest = nil + for { + select { + case <-ctx.Done(): + if errors.Is(ctx.Err(), context.Canceled) { + return nil } - }() - for { - select { - case <-ctx.Done(): - if errors.Is(ctx.Err(), context.Canceled) { - return nil - } + return ctx.Err() - return ctx.Err() + default: + } - default: + err := processServer.RecvMsg(&processingRequest) + 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 s, ok := status.FromError(err); (ok && s.Code() == codes.Canceled) || err == io.EOF { + return nil } - err := ss.RecvMsg(&processingRequest) - 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 { - return nil - } - - log.Warn("external_processing: error receiving request/response: %v\n", err) - return status.Errorf(codes.Unknown, "Error receiving request/response: %v", err) - } + log.Warn("external_processing: error receiving request/response: %v\n", err) + return status.Errorf(codes.Unknown, "Error receiving request/response: %v", err) + } - processingResponse, err = envoyExternalProcessingRequestTypeAssert(&processingRequest) - if err != nil { - log.Error("external_processing: error asserting request type: %v\n", err) - return status.Errorf(codes.Unknown, "Error asserting request type: %v", err) - } + processingResponse, err = envoyExternalProcessingRequestTypeAssert(&processingRequest) + if err != nil { + log.Error("external_processing: error asserting request type: %v\n", err) + return status.Errorf(codes.Unknown, "Error asserting request type: %v", err) + } - switch v := processingRequest.Request.(type) { - case *envoyextproc.ProcessingRequest_RequestHeaders: - processingResponse, currentRequest, blocked, err = ProcessRequestHeaders(ctx, v) - case *envoyextproc.ProcessingRequest_ResponseHeaders: - processingResponse, err = processResponseHeaders(v, currentRequest) - currentRequest = nil // Request is done, reset the current request - } + switch v := processingRequest.Request.(type) { + case *envoyextproc.ProcessingRequest_RequestHeaders: + processingResponse, currentRequest, blocked, err = processRequestHeaders(ctx, v) + case *envoyextproc.ProcessingRequest_ResponseHeaders: + processingResponse, err = processResponseHeaders(v, currentRequest) + currentRequest = nil // Request is done, reset the current request + } - if err != nil { - log.Error("external_processing: error processing request: %v\n", err) - return err - } + if err != nil { + log.Error("external_processing: error processing request: %v\n", err) + return err + } - // End of stream reached, no more data to process - if processingResponse == nil { - log.Debug("external_processing: end of stream reached") - return nil - } + // End of stream reached, no more data to process + if processingResponse == nil { + log.Debug("external_processing: end of stream reached") + return nil + } - if err := ss.SendMsg(processingResponse); err != nil { - log.Warn("external_processing: error sending response (probably because of an Envoy timeout): %v", err) - return status.Errorf(codes.Unknown, "Error sending response (probably because of an Envoy timeout): %v", err) - } + if err := processServer.SendMsg(processingResponse); err != nil { + log.Warn("external_processing: error sending response (probably because of an Envoy timeout): %v", err) + return status.Errorf(codes.Unknown, "Error sending response (probably because of an Envoy timeout): %v", err) + } - if blocked { - log.Debug("external_processing: request blocked, end the stream") - currentRequest = nil - return nil - } + if blocked { + log.Debug("external_processing: request blocked, end the stream") + currentRequest = nil + return nil } } } func envoyExternalProcessingRequestTypeAssert(req *envoyextproc.ProcessingRequest) (*envoyextproc.ProcessingResponse, error) { - switch v := req.Request.(type) { + switch r := req.Request.(type) { case *envoyextproc.ProcessingRequest_RequestHeaders, *envoyextproc.ProcessingRequest_ResponseHeaders: return nil, nil @@ -158,8 +169,6 @@ func envoyExternalProcessingRequestTypeAssert(req *envoyextproc.ProcessingReques }, nil case *envoyextproc.ProcessingRequest_ResponseBody: - r := req.Request.(*envoyextproc.ProcessingRequest_ResponseBody) - // Note: The end of stream bool value is not reliable // Sometimes it's not set to true even if there is no more data to process if r.ResponseBody.GetEndOfStream() { @@ -177,11 +186,11 @@ func envoyExternalProcessingRequestTypeAssert(req *envoyextproc.ProcessingReques }, nil default: - return nil, status.Errorf(codes.Unknown, "Unknown request type: %T", v) + return nil, status.Errorf(codes.Unknown, "Unknown request type: %T", r) } } -func ProcessRequestHeaders(ctx context.Context, req *envoyextproc.ProcessingRequest_RequestHeaders) (*envoyextproc.ProcessingResponse, *currentRequest, bool, error) { +func processRequestHeaders(ctx context.Context, req *envoyextproc.ProcessingRequest_RequestHeaders) (*envoyextproc.ProcessingResponse, *currentRequest, bool, error) { log.Debug("external_processing: received request headers: %v\n", req.RequestHeaders) request, err := newRequest(ctx, req) diff --git a/contrib/envoyproxy/go-control-plane/envoy_test.go b/contrib/envoyproxy/go-control-plane/envoy_test.go index 9393278958..8af05eaab3 100644 --- a/contrib/envoyproxy/go-control-plane/envoy_test.go +++ b/contrib/envoyproxy/go-control-plane/envoy_test.go @@ -370,12 +370,11 @@ func newEnvoyAppsecRig(t *testing.T, traceClient bool, interceptorOpts ...ddgrpc interceptorOpts = append([]ddgrpc.InterceptorOption{ddgrpc.WithServiceName("grpc")}, interceptorOpts...) - server := grpc.NewServer( - grpc.StreamInterceptor(StreamServerInterceptor(interceptorOpts...)), - ) + server := grpc.NewServer() fixtureServer := new(envoyFixtureServer) - envoyextproc.RegisterExternalProcessorServer(server, fixtureServer) + appsecSrv := AppsecEnvoyExternalProcessorServer(fixtureServer) + envoyextproc.RegisterExternalProcessorServer(server, appsecSrv) li, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { diff --git a/contrib/envoyproxy/go-control-plane/example_test.go b/contrib/envoyproxy/go-control-plane/example_test.go index d9837d3091..f1e255dcaf 100644 --- a/contrib/envoyproxy/go-control-plane/example_test.go +++ b/contrib/envoyproxy/go-control-plane/example_test.go @@ -6,12 +6,20 @@ package go_control_plane_test import ( - "google.golang.org/grpc" - "gopkg.in/DataDog/dd-trace-go.v1/contrib/envoyproxy/go-control-plane" "log" "net" + + "google.golang.org/grpc" + + extprocv3 "github.com/envoyproxy/go-control-plane/envoy/service/ext_proc/v3" + gocontrolplane "gopkg.in/DataDog/dd-trace-go.v1/contrib/envoyproxy/go-control-plane" ) +// interface fpr external processing server +type envoyExtProcServer struct { + extprocv3.ExternalProcessorServer +} + func Example_server() { // Create a listener for the server. ln, err := net.Listen("tcp", ":50051") @@ -19,11 +27,13 @@ func Example_server() { 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)) + s := grpc.NewServer() + srv := &envoyExtProcServer{} + + // Register the appsec envoy external processor service + appsecSrv := gocontrolplane.AppsecEnvoyExternalProcessorServer(srv) + extprocv3.RegisterExternalProcessorServer(s, appsecSrv) // ... register your services From c86812b197f59a880ddc9483ab1b1043893184c5 Mon Sep 17 00:00:00 2001 From: Flavien Darche Date: Thu, 12 Dec 2024 16:19:46 +0100 Subject: [PATCH 14/14] apply comments --- contrib/envoyproxy/go-control-plane/envoy.go | 23 +++++++++++--------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/contrib/envoyproxy/go-control-plane/envoy.go b/contrib/envoyproxy/go-control-plane/envoy.go index 80a20ab0e5..52279e0138 100644 --- a/contrib/envoyproxy/go-control-plane/envoy.go +++ b/contrib/envoyproxy/go-control-plane/envoy.go @@ -66,7 +66,6 @@ type currentRequest struct { // If the request is blocked, it sends an immediate response and ends the stream. If an error occurs // during processing, it logs the error and returns an appropriate gRPC status error. func (s *appsecEnvoyExternalProcessorServer) Process(processServer envoyextproc.ExternalProcessor_ProcessServer) error { - var ( ctx = processServer.Context() blocked bool @@ -77,11 +76,13 @@ func (s *appsecEnvoyExternalProcessorServer) Process(processServer envoyextproc. // Close the span when the request is done processing defer func() { - if currentRequest != nil { - log.Warn("external_processing: stream stopped during a request, making sure the current span is closed\n") - currentRequest.span.Finish() - currentRequest = nil + if currentRequest == nil { + return } + + log.Warn("external_processing: stream stopped during a request, making sure the current span is closed\n") + currentRequest.span.Finish() + currentRequest = nil }() for { @@ -92,8 +93,8 @@ func (s *appsecEnvoyExternalProcessorServer) Process(processServer envoyextproc. } return ctx.Err() - default: + // no op } err := processServer.RecvMsg(&processingRequest) @@ -138,11 +139,13 @@ func (s *appsecEnvoyExternalProcessorServer) Process(processServer envoyextproc. return status.Errorf(codes.Unknown, "Error sending response (probably because of an Envoy timeout): %v", err) } - if blocked { - log.Debug("external_processing: request blocked, end the stream") - currentRequest = nil - return nil + if !blocked { + continue } + + log.Debug("external_processing: request blocked, end the stream") + currentRequest = nil + return nil } }