Skip to content

Commit

Permalink
usm: tls: nodejs: Introduce NodeJS TLS support for USM (#24285)
Browse files Browse the repository at this point in the history
* usm: configuration: Added a feature flag to enable node js TLS monitoring

* usm: tls: generalize SSL_read_ex and SSL_write_ex

Moving the shared implementation into helper functions, to allow reuse by other probes
like nodejs, istio, and on. Similar to the change introduced in commit '765c48d'

* usm: tls: Introduce NodeJS TLS tag and counter

Adding to the kernel a TLS tag for NodeJS to be used for marking the connections as
NodeJS TLS connections. Also, in the usermode, introducing a dedicated counter for
NodeJS TLS connections.

* usm: tls: nodejs: Implement skeleton of nodejs monitor

Introducing the skeleton of the nodejs monitor, which registers on process creation and termination,
looks for relevant processes (by verifying the executable path contains '/bin/node'), and on the
matching processes, applies SSL hooks

* usm: tls: nodejs: Run nodejs monitor

The change is initializing the nodejs monitoring and launch it.

* usm: tls: nodejs: tests: Added UT for capturing HTTPs over nodejs

* releasenotes: Added release notes for the new feature

* Fixed CR notes

* Fixed python server
  • Loading branch information
guyarb authored Apr 3, 2024
1 parent ec63c78 commit a0ead78
Show file tree
Hide file tree
Showing 21 changed files with 607 additions and 29 deletions.
1 change: 1 addition & 0 deletions pkg/config/setup/system_probe.go
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ func InitSystemProbeConfig(cfg pkgconfigmodel.Config) {
cfg.BindEnvAndSetDefault(join(smNS, "enable_http2_monitoring"), false)
cfg.BindEnvAndSetDefault(join(smNS, "enable_kafka_monitoring"), false)
cfg.BindEnvAndSetDefault(join(smNS, "tls", "istio", "enabled"), false)
cfg.BindEnv(join(smNS, "tls", "nodejs", "enabled"))
cfg.BindEnvAndSetDefault(join(smjtNS, "enabled"), false)
cfg.BindEnvAndSetDefault(join(smjtNS, "debug"), false)
cfg.BindEnvAndSetDefault(join(smjtNS, "args"), defaultServiceMonitoringJavaAgentArgs)
Expand Down
4 changes: 4 additions & 0 deletions pkg/network/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ type Config struct {
// EnableIstioMonitoring specifies whether USM should monitor Istio traffic
EnableIstioMonitoring bool

// EnableNodeJSMonitoring specifies whether USM should monitor NodeJS TLS traffic
EnableNodeJSMonitoring bool

// EnableGoTLSSupport specifies whether the tracer should monitor HTTPS
// traffic done through Go's standard library's TLS implementation
EnableGoTLSSupport bool
Expand Down Expand Up @@ -333,6 +336,7 @@ func New() *Config {
EnableKafkaMonitoring: cfg.GetBool(join(smNS, "enable_kafka_monitoring")),
EnableNativeTLSMonitoring: cfg.GetBool(join(smNS, "tls", "native", "enabled")),
EnableIstioMonitoring: cfg.GetBool(join(smNS, "tls", "istio", "enabled")),
EnableNodeJSMonitoring: cfg.GetBool(join(smNS, "tls", "nodejs", "enabled")),
MaxUSMConcurrentRequests: uint32(cfg.GetInt(join(smNS, "max_concurrent_requests"))),
MaxHTTPStatsBuffered: cfg.GetInt(join(smNS, "max_http_stats_buffered")),
MaxKafkaStatsBuffered: cfg.GetInt(join(smNS, "max_kafka_stats_buffered")),
Expand Down
27 changes: 27 additions & 0 deletions pkg/network/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1286,6 +1286,33 @@ service_monitoring_config:
})
}

func TestNodeJSMonitoring(t *testing.T) {
t.Run("default value", func(t *testing.T) {
aconfig.ResetSystemProbeConfig(t)
cfg := New()
assert.False(t, cfg.EnableNodeJSMonitoring)
})

t.Run("via yaml", func(t *testing.T) {
aconfig.ResetSystemProbeConfig(t)
cfg := configurationFromYAML(t, `
service_monitoring_config:
tls:
nodejs:
enabled: true
`)
assert.True(t, cfg.EnableNodeJSMonitoring)
})

t.Run("via deprecated ENV variable", func(t *testing.T) {
aconfig.ResetSystemProbeConfig(t)
t.Setenv("DD_SERVICE_MONITORING_CONFIG_TLS_NODEJS_ENABLED", "true")

cfg := New()
assert.True(t, cfg.EnableNodeJSMonitoring)
})
}

func TestMaxUSMConcurrentRequests(t *testing.T) {
t.Run("default value", func(t *testing.T) {
aconfig.ResetSystemProbeConfig(t)
Expand Down
40 changes: 34 additions & 6 deletions pkg/network/ebpf/c/protocols/tls/native-tls.h
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,11 @@ int istio_uretprobe__SSL_read(struct pt_regs *ctx) {
return SSL_read_ret(ctx, ISTIO);
}

SEC("uretprobe/SSL_read")
int nodejs_uretprobe__SSL_read(struct pt_regs *ctx) {
return SSL_read_ret(ctx, NODEJS);
}

SEC("uprobe/SSL_write")
int uprobe__SSL_write(struct pt_regs* ctx) {
ssl_write_args_t args = {0};
Expand Down Expand Up @@ -209,6 +214,11 @@ int istio_uretprobe__SSL_write(struct pt_regs* ctx) {
return SSL_write_ret(ctx, ISTIO);
}

SEC("uretprobe/SSL_write")
int nodejs_uretprobe__SSL_write(struct pt_regs* ctx) {
return SSL_write_ret(ctx, NODEJS);
}

SEC("uprobe/SSL_read_ex")
int uprobe__SSL_read_ex(struct pt_regs* ctx) {
ssl_read_ex_args_t args = {0};
Expand All @@ -224,8 +234,7 @@ int uprobe__SSL_read_ex(struct pt_regs* ctx) {
return 0;
}

SEC("uretprobe/SSL_read_ex")
int uretprobe__SSL_read_ex(struct pt_regs* ctx) {
static __always_inline int SSL_read_ex_ret(struct pt_regs* ctx, __u64 tags) {
u64 pid_tgid = bpf_get_current_pid_tgid();
const int return_code = (int)PT_REGS_RC(ctx);
if (return_code != 1) {
Expand Down Expand Up @@ -267,13 +276,23 @@ int uretprobe__SSL_read_ex(struct pt_regs* ctx) {
// We want to guarantee write-TLS hooks generates the same connection tuple, while read-TLS hooks generate
// the inverse direction, thus we're normalizing the tuples into a client <-> server direction.
normalize_tuple(&copy);
tls_process(ctx, &copy, buffer_ptr, bytes_count, LIBSSL);
tls_process(ctx, &copy, buffer_ptr, bytes_count, tags);
return 0;
cleanup:
bpf_map_delete_elem(&ssl_read_ex_args, &pid_tgid);
return 0;
}

SEC("uretprobe/SSL_read_ex")
int uretprobe__SSL_read_ex(struct pt_regs* ctx, __u64 tags) {
return SSL_read_ex_ret(ctx, LIBSSL);
}

SEC("uretprobe/SSL_read_ex")
int nodejs_uretprobe__SSL_read_ex(struct pt_regs *ctx) {
return SSL_read_ex_ret(ctx, NODEJS);
}

SEC("uprobe/SSL_write_ex")
int uprobe__SSL_write_ex(struct pt_regs* ctx) {
ssl_write_ex_args_t args = {0};
Expand All @@ -286,8 +305,7 @@ int uprobe__SSL_write_ex(struct pt_regs* ctx) {
return 0;
}

SEC("uretprobe/SSL_write_ex")
int uretprobe__SSL_write_ex(struct pt_regs* ctx) {
static __always_inline int SSL_write_ex_ret(struct pt_regs* ctx, __u64 tags) {
u64 pid_tgid = bpf_get_current_pid_tgid();
const int return_code = (int)PT_REGS_RC(ctx);
if (return_code != 1) {
Expand Down Expand Up @@ -328,13 +346,23 @@ int uretprobe__SSL_write_ex(struct pt_regs* ctx) {
// to the server <-> client direction.
normalize_tuple(&copy);
flip_tuple(&copy);
tls_process(ctx, &copy, buffer_ptr, bytes_count, LIBSSL);
tls_process(ctx, &copy, buffer_ptr, bytes_count, tags);
return 0;
cleanup:
bpf_map_delete_elem(&ssl_write_ex_args, &pid_tgid);
return 0;
}

SEC("uretprobe/SSL_write_ex")
int uretprobe__SSL_write_ex(struct pt_regs* ctx) {
return SSL_write_ex_ret(ctx, LIBSSL);
}

SEC("uretprobe/SSL_write_ex")
int nodejs_uretprobe__SSL_write_ex(struct pt_regs *ctx) {
return SSL_write_ex_ret(ctx, NODEJS);
}

SEC("uprobe/SSL_shutdown")
int uprobe__SSL_shutdown(struct pt_regs *ctx) {
void *ssl_ctx = (void *)PT_REGS_PARM1(ctx);
Expand Down
1 change: 1 addition & 0 deletions pkg/network/ebpf/c/protocols/tls/tags-types.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ enum static_tags {
JAVA_TLS = (1<<3),
CONN_TLS = (1<<4),
ISTIO = (1<<5),
NODEJS = (1<<6),
};

#endif
5 changes: 4 additions & 1 deletion pkg/network/protocols/http/testutil/pythonserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@ class RequestHandler(http.server.BaseHTTPRequestHandler):
daemon_threads = True
def do_GET(self):
status_code = int(self.path.split("/")[1])
path = self.path
if self.path.startswith("/status"):
path = self.path.split("/status")[1]
status_code = int(path.split("/")[1])
self.send_response(status_code)
self.send_header('Content-type', 'application/octet-stream')
self.send_header('Content-Length', '0')
Expand Down
26 changes: 14 additions & 12 deletions pkg/network/protocols/http/tls_counter.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,25 @@ import (
// TLSCounter is a TLS aware counter, it has a plain counter and a counter for each TLS library
// It enables the use of a single metric that increments based on the TLS library, avoiding the need for separate metrics for each TLS library
type TLSCounter struct {
counterPlain *libtelemetry.Counter
counterGnuTLS *libtelemetry.Counter
counterOpenSLL *libtelemetry.Counter
counterJavaTLS *libtelemetry.Counter
counterGoTLS *libtelemetry.Counter
counterIstioTLS *libtelemetry.Counter
counterPlain *libtelemetry.Counter
counterGnuTLS *libtelemetry.Counter
counterOpenSLL *libtelemetry.Counter
counterJavaTLS *libtelemetry.Counter
counterGoTLS *libtelemetry.Counter
counterIstioTLS *libtelemetry.Counter
counterNodeJSTLS *libtelemetry.Counter
}

// NewTLSCounter creates and returns a new instance of TLSCounter
func NewTLSCounter(metricGroup *libtelemetry.MetricGroup, metricName string, tags ...string) *TLSCounter {
return &TLSCounter{
// tls_library:none is a must, as prometheus metrics must have the same cardinality of tags
counterPlain: metricGroup.NewCounter(metricName, append(tags, "encrypted:false", "tls_library:none")...),
counterGnuTLS: metricGroup.NewCounter(metricName, append(tags, "encrypted:true", "tls_library:gnutls")...),
counterOpenSLL: metricGroup.NewCounter(metricName, append(tags, "encrypted:true", "tls_library:openssl")...),
counterJavaTLS: metricGroup.NewCounter(metricName, append(tags, "encrypted:true", "tls_library:java")...),
counterGoTLS: metricGroup.NewCounter(metricName, append(tags, "encrypted:true", "tls_library:go")...),
counterIstioTLS: metricGroup.NewCounter(metricName, append(tags, "encrypted:true", "tls_library:istio")...),
counterPlain: metricGroup.NewCounter(metricName, append(tags, "encrypted:false", "tls_library:none")...),
counterGnuTLS: metricGroup.NewCounter(metricName, append(tags, "encrypted:true", "tls_library:gnutls")...),
counterOpenSLL: metricGroup.NewCounter(metricName, append(tags, "encrypted:true", "tls_library:openssl")...),
counterJavaTLS: metricGroup.NewCounter(metricName, append(tags, "encrypted:true", "tls_library:java")...),
counterGoTLS: metricGroup.NewCounter(metricName, append(tags, "encrypted:true", "tls_library:go")...),
counterIstioTLS: metricGroup.NewCounter(metricName, append(tags, "encrypted:true", "tls_library:istio")...),
counterNodeJSTLS: metricGroup.NewCounter(metricName, append(tags, "encrypted:true", "tls_library:nodejs")...),
}
}
2 changes: 2 additions & 0 deletions pkg/network/protocols/http/tls_counter_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ func (t *TLSCounter) Add(tx Transaction) {
t.counterGoTLS.Add(1)
case Istio:
t.counterIstioTLS.Add(1)
case NodeJS:
t.counterNodeJSTLS.Add(1)
default:
t.counterPlain.Add(1)
}
Expand Down
2 changes: 2 additions & 0 deletions pkg/network/protocols/http/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const (
Java ConnTag = C.JAVA_TLS
TLS ConnTag = C.CONN_TLS
Istio ConnTag = C.ISTIO
NodeJS ConnTag = C.NODEJS
)

var (
Expand All @@ -44,5 +45,6 @@ var (
Java: "tls.library:java",
TLS: "tls.connection:encrypted",
Istio: "tls.library:istio",
NodeJS: "tls.library:nodejs",
}
)
2 changes: 2 additions & 0 deletions pkg/network/protocols/http/types_linux.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions pkg/network/protocols/testutil/serverutils.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@
package testutil

import (
"bytes"
"context"
"fmt"
"os/exec"
"regexp"
"strconv"
"strings"
"testing"
"time"

Expand All @@ -21,6 +24,19 @@ const (
DefaultTimeout = time.Minute
)

// GetDockerPID returns the PID of a docker container.
func GetDockerPID(dockerName string) (int64, error) {
// Ensuring no previous instances exists.
c := exec.Command("docker", "inspect", "-f", "{{.State.Pid}}", dockerName)
var stdout, stderr bytes.Buffer
c.Stdout = &stdout
c.Stderr = &stderr
if err := c.Run(); err != nil {
return 0, fmt.Errorf("failed to get %s pid: %s", dockerName, stderr.String())
}
return strconv.ParseInt(strings.TrimSpace(stdout.String()), 10, 64)
}

// RunDockerServer is a template for running a protocols server in a docker.
// - serverName is a friendly name of the server we are setting (AMQP, mongo, etc.).
// - dockerPath is the path for the docker-compose.
Expand Down
68 changes: 68 additions & 0 deletions pkg/network/protocols/tls/nodejs/nodejs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// 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-present Datadog, Inc.

// Package nodejs provides helpers to run nodejs HTTPs server.
package nodejs

import (
"io"
"os"
"regexp"
"testing"

"github.com/DataDog/datadog-agent/pkg/network/protocols/http/testutil"
protocolsUtils "github.com/DataDog/datadog-agent/pkg/network/protocols/testutil"
)

func copyFile(src, dst string) error {
source, err := os.Open(src)
if err != nil {
return err
}
defer source.Close()

destination, err := os.Create(dst)
if err != nil {
return err
}
defer destination.Close()

_, err = io.Copy(destination, source)
return err
}

func linkFile(t *testing.T, src, dst string) error {
t.Helper()
_ = os.Remove(dst)
if err := copyFile(src, dst); err != nil {
return err
}
t.Cleanup(func() { os.Remove(dst) })
return nil
}

// RunServerNodeJS launches an HTTPs server written in NodeJS.
func RunServerNodeJS(t *testing.T, key, cert, serverPort string) error {
t.Helper()
dir, _ := testutil.CurDir()
if err := linkFile(t, key, dir+"/testdata/certs/srv.key"); err != nil {
return err
}
if err := linkFile(t, cert, dir+"/testdata/certs/srv.crt"); err != nil {
return err
}
env := []string{
"ADDR=0.0.0.0",
"PORT=" + serverPort,
"CERTS_DIR=/v/certs",
"TESTDIR=" + dir + "/testdata",
}
return protocolsUtils.RunDockerServer(t, "nodejs-server", dir+"/testdata/docker-compose.yml", env, regexp.MustCompile("Server running at https.*"), protocolsUtils.DefaultTimeout, 3)
}

// GetNodeJSDockerPID returns the PID of the nodejs docker container.
func GetNodeJSDockerPID() (int64, error) {
return protocolsUtils.GetDockerPID("node-node-1")
}
Empty file.
13 changes: 13 additions & 0 deletions pkg/network/protocols/tls/nodejs/testdata/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
version: '3'
name: node
services:
node:
image: node:lts-alpine3.19
command: ["node", "/v/server.js"]
ports:
- ${PORT}:4141
environment:
- ADDR
- CERTS_DIR
volumes:
- ${TESTDIR}:/v:z
Loading

0 comments on commit a0ead78

Please sign in to comment.