Skip to content

Commit

Permalink
Check if required capabilities are available (#1067)
Browse files Browse the repository at this point in the history
Check whether the required capabilities are available upon starting up,
and bail early with a friendly message otherwise.
  • Loading branch information
rafaelroquetto authored Aug 2, 2024
1 parent 07fde50 commit 93aac32
Show file tree
Hide file tree
Showing 14 changed files with 460 additions and 11 deletions.
9 changes: 9 additions & 0 deletions cmd/beyla/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,15 @@ func main() {
os.Exit(-1)
}

if err := beyla.CheckOSCapabilities(config); err != nil {
if config.EnforceSysCaps {
slog.Error("can't start Beyla", "error", err)
os.Exit(-1)
}

slog.Warn("Required system capabilities not present, Beyla may malfunction", "error", err)
}

if config.ProfilePort != 0 {
go func() {
slog.Info("starting PProf HTTP listener", "port", config.ProfilePort)
Expand Down
30 changes: 21 additions & 9 deletions docs/sources/configure/options.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,18 @@ formats are:
| `json` | prints a compact JSON object |
| `json_indent` | prints an indented JSON object |

| YAML | Environment variable | Type | Default |
| ----------------- | ------------------------ | -------- | ---------- |
| `enforce_sys_caps` | `BEYLA_ENFORCE_SYS_CAPS` | boolean | `true` |

<a id="caps"></a>

If you have set the `enforce_sys_caps` to true, if the required system
capabilities are not present Beyla aborts its startup and logs a list of the
missing capabilities.

If you have set the configuration option to `false`, Beyla logs a list of the
missing capabilities only.

## Service discovery

Expand All @@ -213,7 +225,7 @@ namespace.
For more details about this section, go to the [discovery services section](#discovery-services-section)
of this document.

| YAML | Environment variable | Type | Default |
| YAML | Environment variable | Type | Default |
| -------------------------- | -------------------------------- | ------- | ------- |
| `skip_go_specific_tracers` | `BEYLA_SKIP_GO_SPECIFIC_TRACERS` | boolean | false |

Expand Down Expand Up @@ -552,7 +564,7 @@ attributes:
dns: false
```

| YAML | Environment variable | Type | Default |
| YAML | Environment variable | Type | Default |
| ----- | ------------------------------- | ------- | ------- |
| `dns` | `BEYLA_HOSTNAME_DNS_RESOLUTION` | boolean | `true` |

Expand Down Expand Up @@ -605,7 +617,7 @@ It is IMPORTANT to consider that enabling this feature requires a previous step
providing some extra permissions to the Beyla Pod. Consult the
["Configuring Kubernetes metadata decoration section" in the "Running Beyla in Kubernetes"]({{< relref "../setup/kubernetes.md" >}}) page.

| YAML | Environment variable | Type | Default |
| YAML | Environment variable | Type | Default |
| -------- | ---------------------------- | ------- | ------- |
| `enable` | `BEYLA_KUBE_METADATA_ENABLE` | boolean | `false` |

Expand Down Expand Up @@ -818,9 +830,9 @@ If this property is not provided, Beyla will guess it according to the following
- Beyla will guess `http/protobuf` if the port ends in `4318` (`4318`, `14318`, `24318`, ...),
as `4318` is the usual Port number for the OTEL HTTP collector.

| YAML | Environment variable | Type | Default |
| ---------------------- | --------------------------------- | ---- | ------- |
| `insecure_skip_verify` | `BEYLA_OTEL_INSECURE_SKIP_VERIFY` | bool | `false` |
| YAML | Environment variable | Type | Default |
| ---------------------- | --------------------------------- | ------- | ------- |
| `insecure_skip_verify` | `BEYLA_OTEL_INSECURE_SKIP_VERIFY` | boolean | `false` |

Controls whether the OTEL client verifies the server's certificate chain and host name.
If set to `true`, the OTEL client accepts any certificate presented by the server
Expand Down Expand Up @@ -1018,9 +1030,9 @@ If this property is not provided, Beyla will guess it according to the following
- Beyla will guess `http/protobuf` if the port ends in `4318` (`4318`, `14318`, `24318`, ...),
as `4318` is the usual Port number for the OTEL HTTP collector.
| YAML | Environment variable | Type | Default |
| ---------------------- | --------------------------------- | ---- | ------- |
| `insecure_skip_verify` | `BEYLA_OTEL_INSECURE_SKIP_VERIFY` | bool | `false` |
| YAML | Environment variable | Type | Default |
| ---------------------- | --------------------------------- | ------- | ------- |
| `insecure_skip_verify` | `BEYLA_OTEL_INSECURE_SKIP_VERIFY` | boolean | `false` |
Controls whether the OTEL client verifies the server's certificate chain and host name.
If set to `true`, the OTEL client accepts any certificate presented by the server
Expand Down
6 changes: 6 additions & 0 deletions pkg/beyla/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const (
var DefaultConfig = Config{
ChannelBufferLen: 10,
LogLevel: "INFO",
EnforceSysCaps: true,
EBPF: ebpfcommon.TracerConfig{
BatchLength: 100,
BatchTimeout: time.Second,
Expand Down Expand Up @@ -165,6 +166,11 @@ type Config struct {

LogLevel string `yaml:"log_level" env:"BEYLA_LOG_LEVEL"`

// Check for required system capabilities and bail if they are not
// present. If set to 'false', Beyla will still print a list of missing
// capabilities, but the execution will continue
EnforceSysCaps bool `yaml:"enforce_sys_caps" env:"BEYLA_ENFORCE_SYS_CAPS"`

// From this comment, the properties below will remain undocumented, as they
// are useful for development purposes. They might be helpful for customer support.

Expand Down
1 change: 1 addition & 0 deletions pkg/beyla/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ network:
ServiceName: "svc-name",
ChannelBufferLen: 33,
LogLevel: "INFO",
EnforceSysCaps: true,
Printer: false,
TracePrinter: "json",
EBPF: ebpfcommon.TracerConfig{
Expand Down
88 changes: 88 additions & 0 deletions pkg/beyla/os.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@ package beyla

import (
"fmt"
"strings"

"golang.org/x/sys/unix"

ebpfcommon "github.com/grafana/beyla/pkg/internal/ebpf/common"
"github.com/grafana/beyla/pkg/internal/helpers"
)

// Minimum required Kernel version: 5.8
Expand All @@ -21,3 +25,87 @@ func CheckOSSupport() error {
}
return nil
}

type osCapabilitiesError uint64

func (e *osCapabilitiesError) Set(c helpers.OSCapability) {
*e |= 1 << c
}

func (e *osCapabilitiesError) Clear(c helpers.OSCapability) {
*e &= ^(1 << c)
}

func (e osCapabilitiesError) IsSet(c helpers.OSCapability) bool {
return e&(1<<c) > 0
}

func (e osCapabilitiesError) Empty() bool {
return e == 0
}

func (e osCapabilitiesError) Error() string {
if e == 0 {
return ""
}

var sb strings.Builder

sb.WriteString("the following capabilities are required: ")

sep := ""

for i := helpers.OSCapability(0); i <= unix.CAP_LAST_CAP; i++ {
if e.IsSet(i) {
sb.WriteString(sep)
sb.WriteString(i.String())

sep = ", "
}
}

return sb.String()
}

func CheckOSCapabilities(config *Config) error {
caps, err := helpers.GetCurrentProcCapabilities()

if err != nil {
return fmt.Errorf("unable to query OS capabilities: %w", err)
}

var capError osCapabilitiesError

testAndSet := func(c helpers.OSCapability) {
if !caps.Has(c) {
capError.Set(c)
}
}

// core capabilities
testAndSet(unix.CAP_BPF)
testAndSet(unix.CAP_PERFMON)
testAndSet(unix.CAP_DAC_READ_SEARCH)

major, minor := kernelVersion()

// CAP_SYS_RESOURCE is only required on kernels < 5.11
if (major == 5 && minor < 11) || (major < 5) {
testAndSet(unix.CAP_SYS_RESOURCE)
}

if config.Enabled(FeatureAppO11y) {
testAndSet(unix.CAP_CHECKPOINT_RESTORE)
testAndSet(unix.CAP_SYS_PTRACE)
}

if config.Enabled(FeatureNetO11y) {
testAndSet(unix.CAP_NET_RAW)
}

if capError.Empty() {
return nil
}

return capError
}
111 changes: 111 additions & 0 deletions pkg/beyla/os_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
package beyla

import (
"errors"
"fmt"
"testing"

"github.com/stretchr/testify/assert"
"golang.org/x/sys/unix"

"github.com/grafana/beyla/pkg/internal/helpers"
"github.com/grafana/beyla/pkg/services"
)

type testCase struct {
Expand Down Expand Up @@ -44,3 +49,109 @@ func TestCheckOSSupport_Unsupported(t *testing.T) {
})
}
}

func TestOSCapabilitiesError_Empty(t *testing.T) {
var capErr osCapabilitiesError

assert.True(t, capErr.Empty())
assert.Equal(t, "", capErr.Error())
}

func TestOSCapabilitiesError_Set(t *testing.T) {
var capErr osCapabilitiesError

for c := helpers.OSCapability(0); c <= unix.CAP_LAST_CAP; c++ {
assert.False(t, capErr.IsSet(c))
capErr.Set(c)
assert.True(t, capErr.IsSet(c))
capErr.Clear(c)
assert.False(t, capErr.IsSet(c))
}
}

func TestOSCapabilitiesError_ErrorString(t *testing.T) {
var capErr osCapabilitiesError

assert.Equal(t, "", capErr.Error())

capErr.Set(unix.CAP_BPF)

// no separator (,)
assert.Equal(t, "the following capabilities are required: CAP_BPF", capErr.Error())

capErr.Set(unix.CAP_NET_RAW)

// capabilities appear in ascending order (they are just numeric
// constants) separated by a comma
assert.True(t, unix.CAP_NET_RAW < unix.CAP_BPF)
assert.Equal(t, "the following capabilities are required: CAP_NET_RAW, CAP_BPF", capErr.Error())
}

type capClass int

const (
capCore = capClass(iota + 1)
capApp
capNet
)

type capTestData struct {
osCap helpers.OSCapability
class capClass
kernMaj int
kernMin int
}

var capTests = []capTestData{
{osCap: unix.CAP_BPF, class: capCore},
{osCap: unix.CAP_PERFMON, class: capCore},
{osCap: unix.CAP_DAC_READ_SEARCH, class: capCore},
{osCap: unix.CAP_SYS_RESOURCE, class: capCore, kernMaj: 5, kernMin: 10},
{osCap: unix.CAP_SYS_RESOURCE, class: capCore, kernMaj: 4, kernMin: 11},
{osCap: unix.CAP_CHECKPOINT_RESTORE, class: capApp},
{osCap: unix.CAP_SYS_PTRACE, class: capApp},
{osCap: unix.CAP_NET_RAW, class: capNet},
}

func TestCheckOSCapabilities(t *testing.T) {
caps, err := helpers.GetCurrentProcCapabilities()

assert.NoError(t, err)

// assume this proc doesn't have any caps set (which is usually the case
// for non privileged processes) instead of turning this into a privileged
// test and manually dropping capabilities
assert.Zero(t, caps[0].Effective)
assert.Zero(t, caps[1].Effective)

test := func(data *capTestData) {
overrideKernelVersion(testCase{data.kernMaj, data.kernMin})

cfg := Config{
NetworkFlows: NetworkConfig{Enable: data.class == capNet},
Discovery: services.DiscoveryConfig{SystemWide: data.class == capApp},
}

err := CheckOSCapabilities(&cfg)

if !assert.Error(t, err) {
assert.FailNow(t, "CheckOSCapabilities() should have returned an error")
}

var osCapErr osCapabilitiesError

if !errors.As(err, &osCapErr) {
assert.Fail(t, "CheckOSCapabilities failed", err)
}

assert.True(t, osCapErr.IsSet(data.osCap),
fmt.Sprintf("%s should be present in error", data.osCap.String()))
}

for i := range capTests {
c := capTests[i]
t.Run(fmt.Sprintf("%s %d.%d", c.osCap.String(), c.kernMaj, c.kernMin), func(*testing.T) {
test(&c)
})
}
}
8 changes: 8 additions & 0 deletions pkg/internal/discover/attacher_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (

"github.com/cilium/ebpf/rlimit"
"golang.org/x/sys/unix"

"github.com/grafana/beyla/pkg/internal/helpers"
)

func (ta *TraceAttacher) close() {
Expand Down Expand Up @@ -46,6 +48,12 @@ func (ta *TraceAttacher) bpfMount(pinPath string) error {
return err
}
if !mounted {
caps, err := helpers.GetCurrentProcCapabilities()

if err == nil && !caps.Has(unix.CAP_SYS_ADMIN) {
return fmt.Errorf("beyla requires CAP_SYS_ADMIN in order to mount %s", pinPath)
}

return unix.Mount(pinPath, pinPath, "bpf", 0, "")
}
if !bpffsInstance {
Expand Down
Loading

0 comments on commit 93aac32

Please sign in to comment.