Skip to content

feat(contrib/haproxy): HAProxy AAP support#3912

Merged
dd-mergequeue[bot] merged 18 commits into
mainfrom
flavien/contrib/haproxy
Oct 2, 2025
Merged

feat(contrib/haproxy): HAProxy AAP support#3912
dd-mergequeue[bot] merged 18 commits into
mainfrom
flavien/contrib/haproxy

Conversation

@e-n-0

@e-n-0 e-n-0 commented Aug 25, 2025

Copy link
Copy Markdown
Member

What does this PR do?

This PR implements an HAProxy SPOE messages handler that can be used to create full featured AAP HAProxy agent (coming part of #3913).

This PR uses the message_processor package implemented from #3860 (superseded by #3945 and #3951) and implements its own HAProxy message processor.

This implementation use the library github.com/negasus/haproxy-spoe-go for the communication between haproxy and the agent (implementing the SPOP).

Motivation

New proxy integration supported for AAP: HAProxy

Reviewer's Checklist

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

Unsure? Have a question? Request a review!

@e-n-0
e-n-0 changed the base branch from main to flavien/envoy/refactor-separation August 25, 2025 12:45
@e-n-0
e-n-0 force-pushed the flavien/envoy/refactor-separation branch from 3518fb7 to 049d56e Compare August 25, 2025 12:46
@pr-commenter

pr-commenter Bot commented Aug 25, 2025

Copy link
Copy Markdown

Benchmarks

Benchmark execution time: 2025-10-01 16:19:32

Comparing candidate commit badd77b in PR branch flavien/contrib/haproxy with baseline commit 80ce018 in branch main.

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

@e-n-0
e-n-0 force-pushed the flavien/contrib/haproxy branch from 1c79257 to 122b92f Compare August 25, 2025 13:16
@e-n-0
e-n-0 force-pushed the flavien/envoy/refactor-separation branch from 9483abb to 6db4230 Compare August 25, 2025 13:28
@e-n-0
e-n-0 force-pushed the flavien/contrib/haproxy branch from 469c32d to 9ee5626 Compare August 25, 2025 13:29
@e-n-0 e-n-0 changed the title contrib/haproxy/stream-offload-processing: Add AAP support feat(contrib/haproxy): HAProxy AAP support Aug 25, 2025
@github-actions github-actions Bot added the apm:ecosystem contrib/* related feature requests or bugs label Aug 25, 2025
@datadog-official

datadog-official Bot commented Aug 25, 2025

Copy link
Copy Markdown
Contributor

✅ Tests

🎉 All green!

❄️ No new flaky tests detected
🧪 All tests passed

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: badd77b | Docs | Was this helpful? Give us feedback!

Comment thread contrib/haproxy/stream-processing-offload/haproxy_mp.go Outdated
@e-n-0
e-n-0 force-pushed the flavien/contrib/haproxy branch from 6c40e5f to 5fce2b7 Compare September 8, 2025 16:08
@e-n-0
e-n-0 changed the base branch from flavien/envoy/refactor-separation to main September 17, 2025 16:06
@e-n-0
e-n-0 force-pushed the flavien/contrib/haproxy branch 4 times, most recently from f3ac150 to 2664229 Compare September 19, 2025 15:31
@e-n-0

e-n-0 commented Sep 22, 2025

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. You're on a roll.

About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you open a pull request for review, mark a draft as ready, or comment "@codex review". If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex fix this CI failure" or "@codex address that feedback".

@e-n-0
e-n-0 force-pushed the flavien/contrib/haproxy branch 3 times, most recently from eec6d03 to 39c6213 Compare September 24, 2025 09:42
@e-n-0
e-n-0 marked this pull request as ready for review September 24, 2025 11:39
@e-n-0
e-n-0 requested review from a team as code owners September 24, 2025 11:39
@e-n-0
e-n-0 requested a review from eliottness September 24, 2025 11:39
@e-n-0 e-n-0 self-assigned this Sep 24, 2025
@e-n-0
e-n-0 force-pushed the flavien/contrib/haproxy branch from 39c6213 to db0cd84 Compare September 29, 2025 09:51
Comment on lines +23 to +69
func getStringValue(msg *message.Message, key string) string {
if val, exists := msg.KV.Get(key); exists {
if str, ok := val.(string); ok {
return str
}
}
return ""
}

func getIntValue(msg *message.Message, key string) int {
if val, exists := msg.KV.Get(key); exists {
if i, ok := val.(int); ok {
return i
}
if i64, ok := val.(int64); ok {
return int(i64)
}
}
return 0
}

func getBytesArrayValue(msg *message.Message, key string) []byte {
if val, exists := msg.KV.Get(key); exists {
if bytes, ok := val.([]byte); ok {
return bytes
}
}
return nil
}

func getBoolValue(msg *message.Message, key string) bool {
if val, exists := msg.KV.Get(key); exists {
if b, ok := val.(bool); ok {
return b
}
}
return false
}

func getIPValue(msg *message.Message, key string) net.IP {
if val, exists := msg.KV.Get(key); exists {
if ip, ok := val.(net.IP); ok {
return ip
}
}
return nil
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

In the end is this Message type even usable alone. Maybe create a wrapper type and transform those into methods so it is more convenient

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Change made in 74e2d5e

Comment thread contrib/haproxy/stream-processing-offload/haproxy_utils.go Outdated
Comment thread contrib/haproxy/stream-processing-offload/haproxy_utils.go Outdated
Comment on lines +103 to +113
s, ok := tracer.SpanFromContext(ctx)
if !ok {
return fmt.Errorf("failed to retreive the span from the context of the request")
}

timeout := getStringValue(requestContextData.msg, "timeout")
requestContextData.timeout = timeout

spanId := s.Context().SpanID()
spanIdStr := strconv.FormatUint(spanId, 10)
requestContextData.req.Actions.SetVar(action.ScopeTransaction, "span_id", spanIdStr)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why do you need to get the span id here? Can you add a comment explaining why ?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

As the HAProxy SPO protocol is working by sending messages to the agent. However there is no way to link messages to a request (this doesn't work like envoy in a bi-directional stream). The answer found to link each message was to:

  1. Set the span id as variable inside the answer of the request headers message
  2. Store the current request context linked with this span id
  3. In HAProy, this variable is set in the session of the request, and the span id is sent back again on each other messages related to this request
  4. We retrieve the cached request state on each message based on the received span id

Comment thread contrib/haproxy/stream-processing-offload/haproxy_utils.go Outdated
Comment thread contrib/haproxy/stream-processing-offload/haproxy.go Outdated
Comment thread contrib/haproxy/stream-processing-offload/haproxy.go Outdated
Comment on lines +38 to +41
authority := headers.Get("Host")
method := getStringValue(m.msg, "method")
path := getStringValue(m.msg, "path")
https := getBoolValue(m.msg, "https")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same here about making these strings into documented constants

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Changes made in d3ecca8

Comment thread contrib/haproxy/stream-processing-offload/haproxy_messages.go Outdated
Comment on lines +107 to +108
contentLength := headers.Get("Content-Length")
if contentLength != "" {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
contentLength := headers.Get("Content-Length")
if contentLength != "" {
if contentLength := headers.Get("Content-Length"); contentLength != "" {

Using the content length to know ahead of time if there will be a body is not exactly right I believe. Tons of streaming requests don't set the content-length header and still send a body. Either in the request or the response. cf. https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Length

Although it can be used to know ahead of time if the body will be too big

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

As we discussed, I removed the mandatory content length check to ask for the body. Now the body is always sent if the content type is a valid body we can parse, except if the content-length is detected as 0.
See f1c9872

@e-n-0
e-n-0 force-pushed the flavien/contrib/haproxy branch from a7c40ec to d3ecca8 Compare October 1, 2025 12:10
@e-n-0
e-n-0 requested a review from eliottness October 1, 2025 13:25
requestContextData := &haproxyRequestContextData{req: req, msg: msg}

switch msg.Name {
case "http-request-headers-msg":

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I guess those could also be set as constant since they don't seem to appear as-is in the SPOP protocal but rather seems to be arbitrarily set in the HAProxy config crumbs you created

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Changed in 8729c0f

handler, mt, cleanup := setup()
defer cleanup()

end2EndStreamRequest(t, handler, "/", "GET", map[string]string{"User-Agent": "Chromium", "Content-Type": "application/json"}, map[string]string{}, false, false, `{ "name": "<script>alert(1)</script>" }`, "")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
end2EndStreamRequest(t, handler, "/", "GET", map[string]string{"User-Agent": "Chromium", "Content-Type": "application/json"}, map[string]string{}, false, false, `{ "name": "<script>alert(1)</script>" }`, "")
end2EndStreamRequest(t, handler, "/", "GET", map[string]string{"User-Agent": "Chromium", "Content-Type": "application/json"}, map[string]string{}, false, false, `{ "payload": { "name": "<script>alert(1)</script>" } }`, "")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please rebase on the fix we did yesterday and make sure this test pass

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Changed in badd77b

Comment on lines +378 to +404
// This test is failing because the external processor is waiting for a body to run the waf on the response headers
// This scenario can happen if the processor never receive the body (due to a timeout, error in the app backend, ...)
/*t.Run("blocking-event-on-response-headers-with-body-not-sent", func(t *testing.T) {
handler, mt, cleanup := setup()
defer cleanup()

spanId, bodyRequested, blockedAct := sendProcessingRequestHeaders(t, handler, map[string]string{"User-Agent": "Chromium", "Content-Type": "application/json"}, "GET", "/", 0)
require.False(t, bodyRequested)
require.Nil(t, blockedAct)

// Send a processing response headers with the information that it would be followed by a body, but don't send the body
bodyRequested, blockedAct = sendProcessingResponseHeaders(t, handler, map[string]string{"test": "match-response-header", "Content-Type": "application/json"}, "200", spanId, 256)

// Res should be an immediate response with the blocking event
require.Nil(t, blockedAct)
require.False(t, bodyRequested)

finished := mt.FinishedSpans()
require.Len(t, finished, 1)
checkForAppsecEvent(t, finished, map[string]int{"custom-001": 1, "headers-003": 1})

// Check for tags
span := finished[0]
require.Equal(t, "true", span.Tag("appsec.event"))
require.Equal(t, "true", span.Tag("appsec.blocked"))
})*/
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can you rather skip the test than comment it? Are you sure we can't make this test pass ?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This test scenario is a bit special. In the test we don't send the response body even if that's requested by the processor. We are simulating an error on the HAProxy side where the body is never sent.
In this case when the timeout (of the cache) triggers, the request will be closed and mark as blocked in the trace, but this won't reflect the reality, as the response to the client would have been different (but we don't receive this data).

I can make it pass, but it will validate the wrong behavior

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I did the changes here badd77b

@e-n-0

e-n-0 commented Oct 1, 2025

Copy link
Copy Markdown
Member Author

/merge --delay in 10 hours

@dd-devflow-routing-codex

dd-devflow-routing-codex Bot commented Oct 1, 2025

Copy link
Copy Markdown

View all feedbacks in Devflow UI.

2025-10-01 22:33:12 UTC ℹ️ Start processing command /merge --delay in 10 hours


2025-10-01 22:33:15 UTC ℹ️ MergeQueue: pull request scheduled for Thu, 02 Oct 2025 08:33:12 UTC

Pull Request scheduled to be added to the queue on Thu, 02 Oct 2025 08:33:12 UTC


2025-10-02 08:34:06 UTC ℹ️ MergeQueue: merge request added to the queue

The expected merge time in main is approximately 18m (p90).


2025-10-02 08:50:07 UTC ℹ️ MergeQueue: This merge request was merged

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

apm:ecosystem contrib/* related feature requests or bugs mergequeue-status: done

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants