Skip to content

feat: include iptables block binary in iptables monitor image #3945

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Aug 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 \
Expand Down
30 changes: 30 additions & 0 deletions azure-iptables-monitor/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,42 @@ 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

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"]
215 changes: 78 additions & 137 deletions bpf-prog/azure-block-iptables/cmd/azure-block-iptables/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading
Loading