diff --git a/Makefile b/Makefile index 0bcf4bde88..8492e7e568 100644 --- a/Makefile +++ b/Makefile @@ -35,10 +35,10 @@ REPO_ROOT ?= $(shell git rev-parse --show-toplevel) REVISION ?= $(shell git rev-parse --short HEAD) ACN_VERSION ?= $(shell git describe --exclude "azure-iptables-monitor*" --exclude "azure-ip-masq-merger*" --exclude "azure-ipam*" --exclude "dropgz*" --exclude "zapai*" --exclude "ipv6-hp-bpf*" --exclude "azure-block-iptables*" --tags --always) IPV6_HP_BPF_VERSION ?= $(notdir $(shell git describe --match "ipv6-hp-bpf*" --tags --always)) -AZURE_BLOCK_IPTABLES_VERSION ?= $(notdir $(shell git describe --match "azure-block-iptables*" --tags --always)) +AZURE_BLOCK_IPTABLES_VERSION ?= $(notdir $(shell git describe --match "azure-block-iptables*" --tags --always)) AZURE_IPAM_VERSION ?= $(notdir $(shell git describe --match "azure-ipam*" --tags --always)) AZURE_IP_MASQ_MERGER_VERSION ?= $(notdir $(shell git describe --match "azure-ip-masq-merger*" --tags --always)) -AZURE_IPTABLES_MONITOR_VERSION ?= $(notdir $(shell git describe --match "azure-iptables-monitor*" --tags --always)) +AZURE_IPTABLES_MONITOR_VERSION ?= $(notdir $(shell git describe --match "azure-block-iptables*" --match "azure-iptables-monitor*" --tags --always)) CNI_VERSION ?= $(ACN_VERSION) CNS_VERSION ?= $(ACN_VERSION) NPM_VERSION ?= $(ACN_VERSION) @@ -467,7 +467,8 @@ azure-iptables-monitor-image: ## build azure-iptables-monitor container image. TAG=$(AZURE_IPTABLES_MONITOR_PLATFORM_TAG) \ TARGET=$(OS) \ OS=$(OS) \ - ARCH=$(ARCH) + ARCH=$(ARCH) \ + EXTRA_BUILD_ARGS="--build-arg AZURE_BLOCK_IPTABLES_VERSION=$(AZURE_BLOCK_IPTABLES_VERSION)" azure-iptables-monitor-image-push: ## push azure-iptables-monitor container image. $(MAKE) container-push \ diff --git a/azure-iptables-monitor/Dockerfile b/azure-iptables-monitor/Dockerfile index eb6c6be056..82247fcf21 100644 --- a/azure-iptables-monitor/Dockerfile +++ b/azure-iptables-monitor/Dockerfile @@ -17,6 +17,35 @@ WORKDIR /azure-iptables-monitor COPY ./azure-iptables-monitor . RUN GOOS=$OS CGO_ENABLED=0 go build -a -o /go/bin/iptables-monitor -trimpath -ldflags "-s -w -X main.version="$VERSION"" -gcflags="-dwarflocationlists=true" . +FROM go AS azure-block-iptables +ARG OS +ARG AZURE_BLOCK_IPTABLES_VERSION +ARG ARCH +WORKDIR /azure-container-networking +COPY ./bpf-prog/azure-block-iptables ./bpf-prog/azure-block-iptables +COPY ./go.mod ./go.sum ./ +# Install BPF development dependencies for Azure Linux (mariner) +RUN tdnf install -y llvm clang libbpf-devel gcc binutils glibc +# Set up C include path for BPF +ENV C_INCLUDE_PATH=/usr/include/bpf +# Set up architecture-specific symlinks for cross-compilation support +RUN if [ "$ARCH" = "amd64" ]; then \ + ARCH_DIR=x86_64-linux-gnu; \ + elif [ "$ARCH" = "arm64" ]; then \ + ARCH_DIR=aarch64-linux-gnu; \ + fi && \ + if [ -n "$ARCH_DIR" ] && [ -d "/usr/include/$ARCH_DIR" ]; then \ + for dir in /usr/include/"$ARCH_DIR"/*; do \ + if [ -d "$dir" ]; then \ + ln -sfn "$dir" /usr/include/$(basename "$dir") || echo "Warning: Failed to create symlink for directory $dir" >&2; \ + elif [ -f "$dir" ]; then \ + ln -Tsfn "$dir" /usr/include/$(basename "$dir") || echo "Warning: Failed to create symlink for file $dir" >&2; \ + fi; \ + done; \ + fi +RUN GOOS=$OS CGO_ENABLED=0 go generate ./bpf-prog/azure-block-iptables/... +RUN GOOS=$OS CGO_ENABLED=0 go build -a -o /go/bin/azure-block-iptables -trimpath -ldflags "-s -w -X main.version="$AZURE_BLOCK_IPTABLES_VERSION"" -gcflags="-dwarflocationlists=true" ./bpf-prog/azure-block-iptables/cmd/azure-block-iptables + FROM mariner-core AS iptables RUN tdnf install -y iptables @@ -24,5 +53,6 @@ FROM mariner-distroless AS linux COPY --from=iptables /usr/sbin/*tables* /usr/sbin/ COPY --from=iptables /usr/lib /usr/lib COPY --from=azure-iptables-monitor /go/bin/iptables-monitor azure-iptables-monitor +COPY --from=azure-block-iptables /go/bin/azure-block-iptables azure-block-iptables ENTRYPOINT ["/azure-iptables-monitor"] diff --git a/bpf-prog/azure-block-iptables/cmd/azure-block-iptables/main.go b/bpf-prog/azure-block-iptables/cmd/azure-block-iptables/main.go index 30f5f3aab7..5a587dbf61 100644 --- a/bpf-prog/azure-block-iptables/cmd/azure-block-iptables/main.go +++ b/bpf-prog/azure-block-iptables/cmd/azure-block-iptables/main.go @@ -4,182 +4,123 @@ package main import ( - "context" + "flag" "fmt" "log" "os" - "os/signal" - "path/filepath" - "syscall" - "time" "github.com/Azure/azure-container-networking/bpf-prog/azure-block-iptables/pkg/bpfprogram" - "github.com/cilium/ebpf/rlimit" - "github.com/fsnotify/fsnotify" + "github.com/pkg/errors" ) -const ( - DefaultConfigFile = "/etc/cni/net.d/iptables-allow-list" +// ProgramVersion is set during build +var ( + version = "unknown" + ErrModeRequired = errors.New("mode is required") + ErrInvalidMode = errors.New("invalid mode. Use -mode=attach or -mode=detach") ) -// BlockConfig holds configuration for the application -type BlockConfig struct { - ConfigFile string +// Config holds configuration for the application +type Config struct { + Mode string // "attach" or "detach" + Overwrite bool // force detach before attach AttacherFactory bpfprogram.AttacherFactory } -// NewDefaultBlockConfig creates a new BlockConfig with default values -func NewDefaultBlockConfig() *BlockConfig { - return &BlockConfig{ - ConfigFile: DefaultConfigFile, - AttacherFactory: bpfprogram.NewProgram, - } -} +// parseArgs parses command line arguments and returns the configuration +func parseArgs() (*Config, error) { + var ( + mode = flag.String("mode", "", "Operation mode: 'attach' or 'detach' (required)") + overwrite = flag.Bool("overwrite", false, "Force detach before attach (only applies to attach mode)") + showVersion = flag.Bool("version", false, "Show version information") + showHelp = flag.Bool("help", false, "Show help information") + ) -// isFileEmptyOrMissing checks if the config file exists and has content -// Returns: 1 if empty, 0 if has content, -1 if missing/error -func isFileEmptyOrMissing(filename string) int { - stat, err := os.Stat(filename) - if err != nil { - if os.IsNotExist(err) { - log.Printf("Config file %s does not exist", filename) - return -1 // File missing - } - log.Printf("Error checking file %s: %v", filename, err) - return -1 // Treat errors as missing - } + flag.Parse() - if stat.Size() == 0 { - log.Printf("Config file %s is empty", filename) - return 1 // File empty + if *showVersion { + fmt.Printf("azure-block-iptables version %s\n", version) + os.Exit(0) } - log.Printf("Config file %s has content (size: %d bytes)", filename, stat.Size()) - return 0 // File exists and has content -} + if *showHelp { + flag.PrintDefaults() + os.Exit(0) + } -// setupFileWatcher sets up a file watcher for the config file -func setupFileWatcher(configFile string) (*fsnotify.Watcher, error) { - watcher, err := fsnotify.NewWatcher() - if err != nil { - return nil, fmt.Errorf("failed to create file watcher: %w", err) + if *mode == "" { + return nil, ErrModeRequired } - // Watch the directory containing the config file - dir := filepath.Dir(configFile) - err = watcher.Add(dir) - if err != nil { - watcher.Close() - return nil, fmt.Errorf("failed to add watch for directory %s: %w", dir, err) + if *mode != "attach" && *mode != "detach" { + return nil, ErrInvalidMode } - log.Printf("Watching directory %s for changes to %s", dir, configFile) - return watcher, nil + return &Config{ + Mode: *mode, + Overwrite: *overwrite, + AttacherFactory: bpfprogram.NewProgram, + }, nil } -func checkFileStatusAndUpdateBPF(configFile string, bp bpfprogram.Attacher) { - // Check current state and take action - fileState := isFileEmptyOrMissing(configFile) - switch fileState { - case 1: // File is empty - log.Println("File is empty, attaching BPF program") - if err := bp.Attach(); err != nil { // No-op if already attached - log.Printf("Failed to attach BPF program: %v", err) - } - case 0: // File has content - log.Println("File has content, detaching BPF program") - if err := bp.Detach(); err != nil { // No-op if already detached - log.Printf("Failed to detach BPF program: %v", err) - } - case -1: // File is missing - log.Println("Config file was deleted, detaching BPF program") - if err := bp.Detach(); err != nil { // No-op if already detached - log.Printf("Failed to detach BPF program: %v", err) +// attachMode handles the attach operation +func attachMode(config *Config) error { + log.Println("Starting attach mode...") + + // Initialize BPF program attacher using the factory + bp := config.AttacherFactory() + + // If overwrite is enabled, first detach any existing programs + if config.Overwrite { + log.Println("Overwrite mode enabled, detaching any existing programs first...") + if err := bp.Detach(); err != nil { + log.Printf("Warning: failed to detach existing programs: %v", err) } } -} -// handleFileEvent processes file system events -func handleFileEvent(event fsnotify.Event, configFile string, bp bpfprogram.Attacher) { - // Check if the event is for our config file - if filepath.Base(event.Name) != filepath.Base(configFile) { - return + // Attach the BPF program + if err := bp.Attach(); err != nil { + return errors.Wrap(err, "failed to attach BPF program") } - log.Printf("Config file changed: %s (operation: %s)", event.Name, event.Op) - - // Small delay to handle rapid successive events - time.Sleep(100 * time.Millisecond) - checkFileStatusAndUpdateBPF(configFile, bp) + log.Println("BPF program attached successfully") + return nil } -// run is the main application logic, separated for easier testing -func run(config *BlockConfig) error { - log.Printf("Using config file: %s", config.ConfigFile) - - // Remove memory limit for eBPF - if err := rlimit.RemoveMemlock(); err != nil { - return fmt.Errorf("failed to remove memlock rlimit: %w", err) - } +// detachMode handles the detach operation +func detachMode(config *Config) error { + log.Println("Starting detach mode...") // Initialize BPF program attacher using the factory bp := config.AttacherFactory() - defer bp.Close() - - // Check initial state of the config file - checkFileStatusAndUpdateBPF(config.ConfigFile, bp) - // Setup file watcher - watcher, err := setupFileWatcher(config.ConfigFile) - if err != nil { - return fmt.Errorf("failed to setup file watcher: %w", err) + // Detach the BPF program + if err := bp.Detach(); err != nil { + return errors.Wrap(err, "failed to detach BPF program") } - defer watcher.Close() - - // Setup signal handling - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) - - log.Println("Starting file watch loop...") - - // Main event loop - for { - select { - case event, ok := <-watcher.Events: - if !ok { - log.Println("Watcher events channel closed") - return nil - } - handleFileEvent(event, config.ConfigFile, bp) - - case err, ok := <-watcher.Errors: - if !ok { - log.Println("Watcher errors channel closed") - return nil - } - log.Printf("Watcher error: %v", err) - - case sig := <-sigChan: - log.Printf("Received signal: %v", sig) - cancel() - return nil - - case <-ctx.Done(): - log.Println("Context cancelled, exiting") - return nil - } + + log.Println("BPF program detached successfully") + return nil +} + +// run is the main application logic +func run(config *Config) error { + switch config.Mode { + case "attach": + return attachMode(config) + case "detach": + return detachMode(config) + default: + return ErrInvalidMode } } func main() { - config := NewDefaultBlockConfig() - - // Parse command line arguments - if len(os.Args) > 1 { - config.ConfigFile = os.Args[1] + config, err := parseArgs() + if err != nil { + log.Printf("Error parsing arguments: %v", err) + flag.PrintDefaults() + os.Exit(1) } if err := run(config); err != nil { diff --git a/bpf-prog/azure-block-iptables/cmd/azure-block-iptables/main_test.go b/bpf-prog/azure-block-iptables/cmd/azure-block-iptables/main_test.go index ad8e9416e3..78b14d9671 100644 --- a/bpf-prog/azure-block-iptables/cmd/azure-block-iptables/main_test.go +++ b/bpf-prog/azure-block-iptables/cmd/azure-block-iptables/main_test.go @@ -4,57 +4,41 @@ package main import ( - "os" "testing" "github.com/Azure/azure-container-networking/bpf-prog/azure-block-iptables/pkg/bpfprogram" - "github.com/fsnotify/fsnotify" - "github.com/pkg/errors" ) func TestHandleFileEventWithMock(t *testing.T) { // Create a mock Attacher mockAttacher := bpfprogram.NewMockProgram() - // Create a temporary config file for testing - configFile := "/tmp/test-iptables-allow-list" - // Test cases testCases := []struct { name string - setupFile func(string) error + mode string + overwrite bool expectedAttach int expectedDetach int }{ { - name: "empty file triggers attach", - setupFile: func(path string) error { - // Create empty file - file, err := os.Create(path) - if err != nil { - return errors.Wrap(err, "failed to create file") - } - return file.Close() - }, + name: "test attach mode", + mode: "attach", + overwrite: false, expectedAttach: 1, expectedDetach: 0, }, { - name: "file with content triggers detach", - setupFile: func(path string) error { - // Create file with content - return os.WriteFile(path, []byte("some content"), 0o600) - }, - expectedAttach: 0, + name: "test attach mode with overwrite", + mode: "attach", + overwrite: true, + expectedAttach: 1, expectedDetach: 1, }, { - name: "missing file triggers detach", - setupFile: func(path string) error { - // Remove file if it exists - os.Remove(path) - return nil - }, + name: "test detach mode", + mode: "detach", + overwrite: false, expectedAttach: 0, expectedDetach: 1, }, @@ -65,20 +49,9 @@ func TestHandleFileEventWithMock(t *testing.T) { // Reset mock state mockAttacher.Reset() - // Setup file state - if err := tc.setupFile(configFile); err != nil { - t.Fatalf("Failed to setup file: %v", err) + if err := run(&Config{Mode: tc.mode, Overwrite: tc.overwrite, AttacherFactory: func() bpfprogram.Attacher { return mockAttacher }}); err != nil { + t.Errorf("Failed to run: %v", err) } - defer os.Remove(configFile) - - // Create a fake fsnotify event - event := fsnotify.Event{ - Name: configFile, - Op: fsnotify.Write, - } - - // Call the function under test - handleFileEvent(event, configFile, mockAttacher) // Verify expectations if mockAttacher.AttachCallCount() != tc.expectedAttach { diff --git a/bpf-prog/azure-block-iptables/pkg/bpfprogram/interface.go b/bpf-prog/azure-block-iptables/pkg/bpfprogram/interface.go index 479e22d2b5..265d9eb623 100644 --- a/bpf-prog/azure-block-iptables/pkg/bpfprogram/interface.go +++ b/bpf-prog/azure-block-iptables/pkg/bpfprogram/interface.go @@ -3,10 +3,10 @@ package bpfprogram // Attacher defines the interface for BPF program attachment operations. // This interface allows for dependency injection and easier testing with mock implementations. type Attacher interface { - // Attach attaches the BPF program to LSM hooks + // Attach attaches the BPF program to LSM hooks and pins the links and maps Attach() error - // Detach detaches the BPF program from LSM hooks + // Unpins the links and maps (causes detachment) Detach() error // IsAttached returns true if the BPF program is currently attached diff --git a/bpf-prog/azure-block-iptables/pkg/bpfprogram/program.go b/bpf-prog/azure-block-iptables/pkg/bpfprogram/program.go index 030f2f08f1..a69167b044 100644 --- a/bpf-prog/azure-block-iptables/pkg/bpfprogram/program.go +++ b/bpf-prog/azure-block-iptables/pkg/bpfprogram/program.go @@ -12,6 +12,7 @@ import ( blockservice "github.com/Azure/azure-container-networking/bpf-prog/azure-block-iptables/pkg/blockservice" "github.com/cilium/ebpf" "github.com/cilium/ebpf/link" + "github.com/cilium/ebpf/rlimit" "github.com/pkg/errors" ) @@ -20,6 +21,10 @@ const ( BPFMapPinPath = "/sys/fs/bpf/block-iptables" // EventCounterMapName is the name used for pinning the event counter map EventCounterMapName = "iptables_block_event_counter" + // IptablesLegacyBlockProgramName is the name used for pinning the legacy iptables block program + IptablesLegacyBlockProgramName = "iptables_legacy_block" + // IptablesNftablesBlockProgramName is the name used for pinning the nftables block program + IptablesNftablesBlockProgramName = "iptables_nftables_block" // NetNSPath is the path to the host network namespace NetNSPath = "/proc/self/ns/net" ) @@ -75,6 +80,33 @@ func (p *Program) unpinEventCounterMap() error { return nil } +// unpinLinks unpins the links to BPF programs from the filesystem +func (p *Program) unpinLinks() error { + var errs []error + + // Unpin the legacy iptables block program + legacyPinPath := filepath.Join(BPFMapPinPath, IptablesLegacyBlockProgramName) + if err := os.Remove(legacyPinPath); err != nil && !os.IsNotExist(err) { + errs = append(errs, errors.Wrapf(err, "failed to remove pinned legacy program %s", legacyPinPath)) + } else { + log.Printf("Legacy iptables block program unpinned from %s", legacyPinPath) + } + + // Unpin the nftables block program + nftablesPinPath := filepath.Join(BPFMapPinPath, IptablesNftablesBlockProgramName) + if err := os.Remove(nftablesPinPath); err != nil && !os.IsNotExist(err) { + errs = append(errs, errors.Wrapf(err, "failed to remove pinned nftables program %s", nftablesPinPath)) + } else { + log.Printf("Nftables block program unpinned from %s", nftablesPinPath) + } + + if len(errs) > 0 { + return errors.Errorf("failed to unpin programs: %v", errs) + } + + return nil +} + func getHostNetnsInode() (uint64, error) { var stat syscall.Stat_t err := syscall.Stat(NetNSPath, &stat) @@ -93,6 +125,11 @@ func (p *Program) Attach() error { return nil } + // Remove memory limit for eBPF + if err := rlimit.RemoveMemlock(); err != nil { + return errors.Wrapf(err, "failed to remove memlock rlimit") + } + log.Println("Attaching BPF program...") // Get the host network namespace inode @@ -149,6 +186,14 @@ func (p *Program) Attach() error { p.objs = nil return errors.Wrap(err, "failed to attach iptables_legacy_block LSM") } + + pinPath := filepath.Join(BPFMapPinPath, IptablesLegacyBlockProgramName) + err = l.Pin(pinPath) + if err != nil { + l.Close() + return errors.Wrap(err, "failed to pin iptables_legacy_block LSM") + } + links = append(links, l) } @@ -167,6 +212,17 @@ func (p *Program) Attach() error { p.objs = nil return errors.Wrap(err, "failed to attach block_nf_netlink LSM") } + pinPath := filepath.Join(BPFMapPinPath, IptablesNftablesBlockProgramName) + err = l.Pin(pinPath) + if err != nil { + for _, link := range links { + link.Close() + } + + l.Close() + return errors.Wrap(err, "failed to pin iptables_nftables_block LSM") + } + links = append(links, l) } @@ -177,38 +233,25 @@ func (p *Program) Attach() error { return nil } -// Detach detaches the BPF program from LSM hooks. func (p *Program) Detach() error { - if !p.attached { - log.Println("BPF program already detached") - return nil - } - - log.Println("Detaching BPF program...") + return p.cleanupPinnedResources() +} - // Unpin the event counter map from filesystem - if err := p.unpinEventCounterMap(); err != nil { - log.Printf("Warning: failed to unpin event counter map: %v", err) - } +// cleanupPinnedResources removes pinned resources even when the program is not currently attached +func (p *Program) cleanupPinnedResources() error { + log.Println("Cleaning up pinned resources...") - // Close all links - for _, l := range p.links { - if err := l.Close(); err != nil { - log.Printf("Warning: failed to close link: %v", err) - } + // Try to unpin links + if err := p.unpinLinks(); err != nil { + log.Printf("Warning: failed to unpin links: %v", err) } - p.links = nil - // Close objects - if p.objs != nil { - if err := p.objs.Close(); err != nil { - log.Printf("Warning: failed to close BPF objects: %v", err) - } - p.objs = nil + // Try to unpin the event counter map + if err := p.unpinEventCounterMap(); err != nil { + log.Printf("Warning: failed to unpin event counter map: %v", err) } - p.attached = false - log.Println("BPF program detached successfully") + log.Println("Pinned resources cleanup completed") return nil } diff --git a/cni/Dockerfile b/cni/Dockerfile index e67b453456..a121f2f318 100644 --- a/cni/Dockerfile +++ b/cni/Dockerfile @@ -6,7 +6,7 @@ ARG OS_VERSION ARG OS # mcr.microsoft.com/oss/go/microsoft/golang:1.23-azurelinux3.0 -FROM --platform=linux/${ARCH} mcr.microsoft.com/oss/go/microsoft/golang@sha256:52da06982153e87569d588c8d25dc519903e92f740fc0d7770f38dfd9f7bb0f2 AS go +FROM --platform=linux/${ARCH} mcr.microsoft.com/oss/go/microsoft/golang@sha256:2ee838d78b546ea43bec72051656ef74c98e6ae17f55be4fb45bd5d9add6dddf AS go # mcr.microsoft.com/azurelinux/base/core:3.0 FROM --platform=linux/${ARCH} mcr.microsoft.com/azurelinux/base/core@sha256:e9bb4e5a79123f2ae29dc601f68adf63a636a455c4259423712b06b798cb201e AS mariner-core diff --git a/cns/Dockerfile b/cns/Dockerfile index 7e132e6632..3f834c42a7 100644 --- a/cns/Dockerfile +++ b/cns/Dockerfile @@ -5,7 +5,7 @@ ARG OS_VERSION ARG OS # mcr.microsoft.com/oss/go/microsoft/golang:1.23-azurelinux3.0 -FROM --platform=linux/${ARCH} mcr.microsoft.com/oss/go/microsoft/golang@sha256:52da06982153e87569d588c8d25dc519903e92f740fc0d7770f38dfd9f7bb0f2 AS go +FROM --platform=linux/${ARCH} mcr.microsoft.com/oss/go/microsoft/golang@sha256:2ee838d78b546ea43bec72051656ef74c98e6ae17f55be4fb45bd5d9add6dddf AS go # mcr.microsoft.com/azurelinux/base/core:3.0 FROM mcr.microsoft.com/azurelinux/base/core@sha256:e9bb4e5a79123f2ae29dc601f68adf63a636a455c4259423712b06b798cb201e AS mariner-core