diff --git a/cmd/config_options.go b/cmd/config_options.go index 947a8194..80176298 100644 --- a/cmd/config_options.go +++ b/cmd/config_options.go @@ -24,7 +24,7 @@ import ( "github.com/go-playground/validator/v10" ) -var validProcessors = []string{"docker", "kubernetes", "kubernetes-in-cluster"} +var validProcessors = []string{"docker", "kubernetes", "kubernetes-in-cluster", "local"} var aliasProcessors = []string{"docker", "k8s", "k8s-ic"} var configOptions *ConfigOptions diff --git a/cmd/local.go b/cmd/local.go new file mode 100644 index 00000000..afa03ddb --- /dev/null +++ b/cmd/local.go @@ -0,0 +1,85 @@ +package cmd + +import ( + "bytes" + "github.com/falcosecurity/driverkit/pkg/driverbuilder" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/spf13/viper" + "golang.org/x/sys/unix" + "log/slog" + "os" + "runtime" + "strings" +) + +// NewLocalCmd creates the `driverkit local` command. +func NewLocalCmd(rootCommand *RootCmd, rootOpts *RootOptions, rootFlags *pflag.FlagSet) *cobra.Command { + localCmd := &cobra.Command{ + Use: "local", + Short: "Build Falco kernel modules and eBPF probes in local env with local kernel sources and gcc/clang.", + PersistentPreRunE: persistentPreRunFunc(rootCommand, rootOpts), + Run: func(c *cobra.Command, args []string) { + slog.With("processor", c.Name()).Info("driver building, it will take a few seconds") + if !configOptions.DryRun { + b := rootOpts.ToBuild() + if !b.HasOutputs() { + return + } + if err := driverbuilder.NewLocalBuildProcessor(viper.GetInt("timeout")).Start(b); err != nil { + slog.With("err", err.Error()).Error("exiting") + os.Exit(1) + } + } + }, + } + // Add root flags, but not the ones unneeded + unusedFlagsSet := map[string]struct{}{ + "architecture": {}, + "kernelrelease": {}, + "kernelversion": {}, + "target": {}, + "kernelurls": {}, + "builderrepo": {}, + "builderimage": {}, + "gccversion": {}, + "kernelconfigdata": {}, + "proxy": {}, + "registry-name": {}, + "registry-password": {}, + "registry-plain-http": {}, + "registry-user": {}, + } + flagSet := pflag.NewFlagSet("local", pflag.ExitOnError) + rootFlags.VisitAll(func(flag *pflag.Flag) { + if _, ok := unusedFlagsSet[flag.Name]; !ok { + flagSet.AddFlag(flag) + } + }) + localCmd.PersistentFlags().AddFlagSet(flagSet) + return localCmd +} + +// Partially overrides rootCmd.persistentPreRunFunc setting some defaults before config init/validation stage. +func persistentPreRunFunc(rootCommand *RootCmd, rootOpts *RootOptions) func(c *cobra.Command, args []string) error { + return func(c *cobra.Command, args []string) error { + // Default values + rootOpts.Target = "local" + u := unix.Utsname{} + if err := unix.Uname(&u); err != nil { + slog.Error("failed to retrieve default uname values", "err", err) + // this only affects logs! + rootOpts.KernelRelease = "1.0.0" + rootOpts.KernelVersion = "1" + } else { + rootOpts.KernelRelease = string(bytes.Trim(u.Release[:], "\x00")) + kv := string(bytes.Trim(u.Version[:], "\x00")) + kv = strings.Trim(kv, "#") + kv = strings.Split(kv, " ")[0] + rootOpts.KernelVersion = kv + } + rootOpts.Architecture = runtime.GOARCH + fn := persistentValidateFunc(rootCommand, rootOpts) + return fn(c, args) + } +} diff --git a/cmd/root.go b/cmd/root.go index 37610025..1171078e 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -184,6 +184,7 @@ func NewRootCmd() *RootCmd { rootCmd.AddCommand(NewKubernetesCmd(rootOpts, flags)) rootCmd.AddCommand(NewKubernetesInClusterCmd(rootOpts, flags)) rootCmd.AddCommand(NewDockerCmd(rootOpts, flags)) + rootCmd.AddCommand(NewLocalCmd(ret, rootOpts, flags)) rootCmd.AddCommand(NewImagesCmd(rootOpts, flags)) rootCmd.AddCommand(NewCompletionCmd()) diff --git a/cmd/testdata/autohelp.txt b/cmd/testdata/autohelp.txt index e72e569b..b293deb7 100644 --- a/cmd/testdata/autohelp.txt +++ b/cmd/testdata/autohelp.txt @@ -1,4 +1,4 @@ -level=INFO msg="specify a valid processor" processors="[docker kubernetes kubernetes-in-cluster]" +level=INFO msg="specify a valid processor" processors="[docker kubernetes kubernetes-in-cluster local]" {{ .Desc }} {{ .Usage }} diff --git a/cmd/testdata/templates/commands.txt b/cmd/testdata/templates/commands.txt index 9a088dfe..89eaee27 100644 --- a/cmd/testdata/templates/commands.txt +++ b/cmd/testdata/templates/commands.txt @@ -4,4 +4,5 @@ Available Commands: help Help about any command images List builder images kubernetes Build Falco kernel modules and eBPF probes against a Kubernetes cluster. - kubernetes-in-cluster Build Falco kernel modules and eBPF probes against a Kubernetes cluster inside a Kubernetes cluster. \ No newline at end of file + kubernetes-in-cluster Build Falco kernel modules and eBPF probes against a Kubernetes cluster inside a Kubernetes cluster. + local Build Falco kernel modules and eBPF probes in local env with local kernel sources and gcc/clang. \ No newline at end of file diff --git a/pkg/driverbuilder/builder/builders.go b/pkg/driverbuilder/builder/builders.go index 9d762eed..82d933e4 100644 --- a/pkg/driverbuilder/builder/builders.go +++ b/pkg/driverbuilder/builder/builders.go @@ -62,6 +62,7 @@ type commonTemplateData struct { ModuleFullPath string BuildModule bool BuildProbe bool + DownloadHeaders bool GCCVersion string } @@ -86,12 +87,12 @@ func Script(b Builder, c Config, kr kernelrelease.KernelRelease) (string, error) return "", err } + var urls []string minimumURLs := 1 if bb, ok := b.(MinimumURLsBuilder); ok { minimumURLs = bb.MinimumURLs() } - var urls []string if c.KernelUrls == nil { urls, err = b.URLs(kr) if err != nil { @@ -254,6 +255,13 @@ func Factory(target Type) (Builder, error) { if strings.HasPrefix(target.String(), "ubuntu") { target = Type("ubuntu") } + // For fake "local" target (that is not exposed to users, + // nor registered in byTarget, just use "arch". + // NOTE: it won't be used anyway, as driverbuilder/local.go + // manually creates a "local" builder. + if target.String() == "local" { + target = Type("arch") + } b, ok := byTarget[target] if !ok { return nil, fmt.Errorf("no builder found for target: %s", target) diff --git a/pkg/driverbuilder/builder/local.go b/pkg/driverbuilder/builder/local.go new file mode 100644 index 00000000..d2f65487 --- /dev/null +++ b/pkg/driverbuilder/builder/local.go @@ -0,0 +1,53 @@ +package builder + +import ( + _ "embed" + "fmt" + "github.com/falcosecurity/driverkit/pkg/kernelrelease" +) + +// NOTE: since this is only used by local build, +// it is not exposed in `target` array, +// so no init() function to register it is present. + +//go:embed templates/local.sh +var localTemplate string + +type LocalBuilder struct { + GccPath string +} + +func (l *LocalBuilder) Name() string { + return "local" +} + +func (l *LocalBuilder) TemplateScript() string { + return localTemplate +} + +func (l *LocalBuilder) URLs(kr kernelrelease.KernelRelease) ([]string, error) { + return nil, nil +} + +func (l *LocalBuilder) MinimumURLs() int { + // We don't need any url + return 0 +} + +type localTemplateData struct { + commonTemplateData +} + +func (l *LocalBuilder) TemplateData(c Config, _ kernelrelease.KernelRelease, _ []string) interface{} { + return localTemplateData{ + commonTemplateData: commonTemplateData{ + DriverBuildDir: DriverDirectory, + ModuleDownloadURL: fmt.Sprintf("%s/%s.tar.gz", c.DownloadBaseURL, c.DriverVersion), + ModuleDriverName: c.DriverName, + ModuleFullPath: ModuleFullPath, + BuildModule: len(c.ModuleFilePath) > 0, + BuildProbe: len(c.ProbeFilePath) > 0, + GCCVersion: l.GccPath, + }, + } +} diff --git a/pkg/driverbuilder/builder/templates/local.sh b/pkg/driverbuilder/builder/templates/local.sh new file mode 100644 index 00000000..2ddbd8bb --- /dev/null +++ b/pkg/driverbuilder/builder/templates/local.sh @@ -0,0 +1,53 @@ +#!/bin/bash +# SPDX-License-Identifier: Apache-2.0 +# +# Copyright (C) 2023 The Falco Authors. +# +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Simple script that desperately tries to load the kernel instrumentation by +# looking for it in a bunch of ways. Convenient when running Falco inside +# a container or in other weird environments. +# +set -xeuo pipefail + +rm -Rf {{ .DriverBuildDir }} +mkdir {{ .DriverBuildDir }} +rm -Rf /tmp/module-download +mkdir -p /tmp/module-download + +curl --silent -SL {{ .ModuleDownloadURL }} | tar -xzf - -C /tmp/module-download +mv /tmp/module-download/*/driver/* {{ .DriverBuildDir }} + +cp /tmp/module-Makefile {{ .DriverBuildDir }}/Makefile +bash /tmp/fill-driver-config.sh {{ .DriverBuildDir }} + +{{ if .BuildModule }} +# Build the module +cd {{ .DriverBuildDir }} +make CC={{ .GCCVersion }} +mv {{ .ModuleDriverName }}.ko {{ .ModuleFullPath }} +strip -g {{ .ModuleFullPath }} +# Print results +modinfo {{ .ModuleFullPath }} +{{ end }} + +{{ if .BuildProbe }} +# Build the eBPF probe +cd {{ .DriverBuildDir }}/bpf +make +ls -l probe.o +{{ end }} + +rm -Rf /tmp/module-download \ No newline at end of file diff --git a/pkg/driverbuilder/local.go b/pkg/driverbuilder/local.go new file mode 100644 index 00000000..4056e642 --- /dev/null +++ b/pkg/driverbuilder/local.go @@ -0,0 +1,160 @@ +package driverbuilder + +import ( + "bufio" + "bytes" + "context" + _ "embed" + "fmt" + "github.com/falcosecurity/driverkit/pkg/driverbuilder/builder" + "log/slog" + "os" + "os/exec" + "path/filepath" + "time" +) + +const LocalBuildProcessorName = "local" + +type LocalBuildProcessor struct { + timeout int +} + +func NewLocalBuildProcessor(timeout int) *LocalBuildProcessor { + return &LocalBuildProcessor{ + timeout: timeout, + } +} + +func (lbp *LocalBuildProcessor) String() string { + return LocalBuildProcessorName +} + +func (lbp *LocalBuildProcessor) Start(b *builder.Build) error { + slog.Debug("doing a new local build") + + // We don't want to download headers + kr := b.KernelReleaseFromBuildConfig() + + // create a builder based on the choosen build type + v := &builder.LocalBuilder{} + c := b.ToConfig() + + // Prepare driver config template + bufFillDriverConfig := bytes.NewBuffer(nil) + err := renderFillDriverConfig(bufFillDriverConfig, driverConfigData{DriverVersion: c.DriverVersion, DriverName: c.DriverName, DeviceName: c.DeviceName}) + if err != nil { + return err + } + + // Prepare makefile template + objList, err := LoadMakefileObjList(c) + if err != nil { + return err + } + bufMakefile := bytes.NewBuffer(nil) + err = renderMakefile(bufMakefile, makefileData{ModuleName: c.DriverName, ModuleBuildDir: builder.DriverDirectory, MakeObjList: objList}) + if err != nil { + return err + } + + // Create all local files + files := []dockerCopyFile{ + {"/tmp/module-Makefile", bufMakefile.String()}, + {"/tmp/fill-driver-config.sh", bufFillDriverConfig.String()}, + } + for _, file := range files { + if err = os.WriteFile(file.Name, []byte(file.Body), 0o755); err != nil { + return err + } + defer os.Remove(file.Name) + } + + defer os.Remove(builder.DriverDirectory) + + // Load gcc versions from system + var gccs []string + if len(b.ModuleFilePath) > 0 { + out, err := exec.Command("which", "gcc").Output() + if err != nil { + return err + } + gccDir := filepath.Dir(string(out)) + proposedGCCs, err := filepath.Glob(gccDir + "/gcc*") + if err != nil { + return err + } + for _, proposedGCC := range proposedGCCs { + // Filter away gcc-{ar,nm,...} + // Only gcc compiler has `-print-search-dirs` option. + gccSearchArgs := fmt.Sprintf(`%s -print-search-dirs 2>&1 | grep "install:"`, proposedGCC) + _, err = exec.Command("bash", "-c", gccSearchArgs).Output() //nolint:gosec // false positive + if err != nil { + continue + } + gccs = append(gccs, proposedGCC) + } + } else { + // We won't use it! + gccs = []string{"gcc"} + } + + for _, gcc := range gccs { + v.GccPath = gcc + + // Generate the build script from the builder + driverkitScript, err := builder.Script(v, c, kr) + if err != nil { + return err + } + ctx, cancelFunc := context.WithTimeout(context.Background(), time.Duration(lbp.timeout)*time.Second) + defer cancelFunc() + cmd := exec.CommandContext(ctx, "/bin/bash", "-c", driverkitScript) + stdout, err := cmd.StdoutPipe() + if err != nil { + slog.Warn("Failed to pipe output. Trying without piping.", "err", err) + _, err = cmd.Output() + } else { + defer stdout.Close() + err = cmd.Start() + if err != nil { + slog.Warn("Failed to execute command.", "err", err) + } else { + // print the output of the subprocess line by line + scanner := bufio.NewScanner(stdout) + for scanner.Scan() { + m := scanner.Text() + fmt.Println(m) + } + err = cmd.Wait() + } + } + if err == nil { + break + } + // If we received an error, perhaps we must just rebuilt the kmod. + // Check if we were able to build anything. + if _, err = os.Stat(builder.ModuleFullPath); !os.IsNotExist(err) { + c.ModuleFilePath = "" + } + if _, err = os.Stat(builder.ProbeFullPath); !os.IsNotExist(err) { + c.ProbeFilePath = "" + } + } + + if len(b.ModuleFilePath) > 0 { + if err = os.Rename(builder.ModuleFullPath, b.ModuleFilePath); err != nil { + return err + } + slog.With("path", b.ModuleFilePath).Info("kernel module available") + } + + if len(b.ProbeFilePath) > 0 { + if err = os.Rename(builder.ProbeFullPath, b.ProbeFilePath); err != nil { + return err + } + slog.With("path", b.ProbeFilePath).Info("eBPF probe available") + } + + return nil +}