diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d8ce4c2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,121 @@ +name: CI Pipeline + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + test: + name: Run Tests + runs-on: ubuntu-latest + + strategy: + matrix: + go-version: ['1.21', '1.22', '1.23'] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: ${{ matrix.go-version }} + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y clang libbpf-dev linux-headers-$(uname -r) + + - name: Cache Go modules + uses: actions/cache@v3 + with: + path: | + ~/.cache/go-build + ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ matrix.go-version }}-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go-${{ matrix.go-version }}- + + - name: Download Go modules + run: go mod download + + - name: Verify Go modules + run: go mod verify + + - name: Run go vet + run: go vet ./... + + - name: Run tests + run: go test -v -race -coverprofile=coverage.out ./... + + - name: Run tests with race detection + run: go test -race -v ./... + + - name: Check test coverage + run: go tool cover -func=coverage.out + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + file: ./coverage.out + flags: unittests + name: codecov-umbrella + fail_ci_if_error: false + + lint: + name: Lint Code + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.23' + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y clang libbpf-dev linux-headers-$(uname -r) + + - name: Run golangci-lint + uses: golangci/golangci-lint-action@v3 + with: + version: latest + args: --timeout=5m + + build: + name: Build Binary + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.23' + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y clang libbpf-dev linux-headers-$(uname -r) + + - name: Build eBPF programs + run: make generate + + - name: Build binary + run: make build + + - name: Build development binary + run: make build-dev + + - name: Test binary can start + run: timeout 5s sudo ./bin/ebpf-server -addr :0 || [[ $? == 124 ]] + diff --git a/Makefile b/Makefile index 4f2f616..0bb47d1 100644 --- a/Makefile +++ b/Makefile @@ -137,10 +137,16 @@ fmt: clean: @echo "Cleaning build artifacts..." rm -rf bin/ - rm -f $(BPF_OBJECTS) - rm -f bpf/*.skel.go - rm -f bpf/include/vmlinux.h.generated - go clean + rm -rf bpf/*.o + +# Generate API documentation using Swagger +.PHONY: docs +docs: + @command -v $(shell go env GOPATH)/bin/swag >/dev/null 2>&1 || { echo "Installing swag..."; go install github.com/swaggo/swag/cmd/swag@latest; } + $(shell go env GOPATH)/bin/swag init -g internal/api/handlers.go -o docs/swagger --parseDependency --parseInternal + @echo "API documentation generated at docs/swagger/" + @echo "Interactive docs: http://localhost:8080/docs/ (when server is running)" + @echo "External docs: https://petstore.swagger.io/?url=https://raw.githubusercontent.com/srodi/ebpf-server/main/docs/swagger.json" # Install the binary system-wide .PHONY: install diff --git a/README.md b/README.md index 2c278c3..a174e85 100644 --- a/README.md +++ b/README.md @@ -1,548 +1,282 @@ # eBPF Network Monitor -An HTTP API server that uses eBPF to monitor network connections and provide real-time network analytics. +[![CI Pipeline](https://github.com/srodi/ebpf-server/actions/workflows/ci.yml/badge.svg)](https://github.com/srodi/ebpf-server/actions/workflows/ci.yml) +[![API Documentation](https://img.shields.io/badge/API-Documentation-blue?style=for-the-badge&logo=swagger)](https://petstore.swagger.io/?url=https://raw.githubusercontent.com/srodi/ebpf-server/main/docs/swagger/swagger.json) +[![OpenAPI Spec](https://img.shields.io/badge/OpenAPI-3.0-green?style=for-the-badge&logo=openapiinitiative)](docs/swagger.json) +[![Go Version](https://img.shields.io/badge/Go-1.23+-00ADD8?style=for-the-badge&logo=go)](https://golang.org) -## Overview +A modular eBPF monitoring system with HTTP API server for real-time network and system event monitoring. Features a clean, interface-based architecture for easy extension with new monitoring programs. -This project implements an HTTP API server that leverages eBPF (Extended Berkeley Packet Filter) technology to monitor network connections at the kernel level. It provides **RESTful endpoints** to retrieve connection statistics and network metrics, making it easy to integrate with monitoring systems, dashboards, and automation tools. - -## Features - -- **eBPF-based Network Monitoring**: Efficient kernel-level network connection tracking -- **REST API**: Simple HTTP endpoints for easy integration -- **Real-time Analytics**: Live network connection statistics and metrics -- **Low Overhead**: Minimal performance impact using eBPF technology -- **JSON Responses**: Structured data with human-readable messages -- **Protocol Detection**: Intelligent identification of TCP/UDP protocols by port - -## Architecture - -``` -┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ -│ HTTP Client │───▶│ HTTP API Server │───▶│ eBPF Programs │ -│ (curl, apps, │ │ (REST) │ │ (Kernel) │ -│ monitoring) │ │ │ │ │ -└─────────────────┘ └─────────────────┘ └─────────────────┘ -``` - -The server consists of: -- **eBPF Programs** (`bpf/`): Kernel-space programs for network monitoring -- **HTTP API** (`internal/api/`): RESTful endpoints for connection analysis -- **eBPF Loader** (`internal/bpf/`): Go bindings for eBPF program management -- **HTTP Server** (`cmd/server/`): Main server with routing and middleware - -## Prerequisites - -### System Requirements -- Linux kernel 4.18+ (for eBPF support) -- Root privileges (required for eBPF programs) - -### Development Dependencies -- Go 1.23.0 or later -- Clang (for compiling eBPF programs) -- libbpf development headers - -### Install Dependencies +## Quick Start -**Ubuntu/Debian:** ```bash -sudo apt update +# Install dependencies (Ubuntu/Debian) sudo apt install -y golang-go clang libbpf-dev linux-headers-$(uname -r) -``` - -**CentOS/RHEL/Fedora:** -```bash -sudo dnf install -y golang clang libbpf-devel kernel-devel -``` -**Arch Linux:** -```bash -sudo pacman -S go clang libbpf linux-headers -``` +# Build and run +make build +sudo ./bin/ebpf-server -**macOS (for development only):** -```bash -brew install go llvm +# Test the API +curl http://localhost:8080/health +curl "http://localhost:8080/api/events?type=connection&limit=10" +curl "http://localhost:8080/api/programs" ``` -## Quick Start - -1. **Clone the repository:** - ```bash - git clone https://github.com/srodi/ebpf-server.git - cd ebpf-server - ``` - -2. **Check system dependencies:** - ```bash - make check-deps - ``` - -3. **Install Go dependencies:** - ```bash - make deps - ``` +**📚 [View Interactive API Documentation](https://petstore.swagger.io/?url=https://raw.githubusercontent.com/srodi/ebpf-server/main/docs/swagger/swagger.json)** - Test APIs directly in your browser -4. **Build the project:** - ```bash - make build - ``` +## Architecture -5. **Run the server (requires root):** - ```bash - sudo make run - ``` - This starts the HTTP API server on port 8080. +**Modular, interface-based monitoring system** with clean separation of concerns: - **Alternative**: Run directly with custom address - ```bash - sudo ./bin/ebpf-server -addr :9090 - ``` +``` +┌───────────────────────────────────────────────────────────┐ +│ System Layer │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ Manager │ │ +│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ +│ │ │ Connection │ │ Packet Drop │ │ Your New │ │ │ +│ │ │ Program │ │ Program │ │ Program │ │ │ +│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │ +│ └─────────────────────────────────────────────────────┘ │ +└───────────────────────────────────────────────────────────┘ + │ + ┌────────┴────────┐ + │ │ + ┌────────▼────────┐ ┌─────▼─────┐ + │ Event Storage │ │ HTTP API │ + │ (Unified) │ │ Handlers │ + └─────────────────┘ └───────────┘ +``` -## Usage +**Core Components:** +- **Core Interfaces**: Define contracts for Events, Programs, Managers, and Storage +- **Event System**: Unified event creation, streaming, and storage with `BaseEvent` +- **Program Manager**: Coordinates program lifecycle and provides unified event streams +- **Storage Layer**: Persistent event storage with query capabilities +- **API Layer**: HTTP endpoints for querying events and program status +- **System Layer**: Top-level coordination and initialization -### HTTP API Endpoints +## Extending the System -The server provides a REST API on port 8080 (default) with the following endpoints: +📚 **[Complete Development Guide](docs/program-development.md)** - Detailed guide for creating new eBPF monitoring programs -#### GET /health +### Quick Example: Create a New Monitoring Program -Health check endpoint to verify the server is running. +### 1. Create a New Monitoring Program -**Response:** -```json -{ - "status": "healthy", - "service": "ebpf-server", - "version": "v1.0.0" -} +```bash +mkdir -p internal/programs/your_monitor ``` -#### POST /api/connection-summary - -Get connection attempt statistics for a specific process over a time period. - -**Important**: This endpoint captures `connect()` syscall attempts, not actual network latency. It counts how many times a process attempted to establish connections. - -**Request Body:** -```json -{ - "pid": 1234, // Process ID (optional, use either pid OR command) - "command": "curl", // Command name (optional, use either pid OR command) - "duration": 60 // Duration in seconds (1-3600, required) +### 2. Implement Your Program + +Create `internal/programs/your_monitor/your_monitor.go`: + +```go +package your_monitor + +import ( + "context" + "encoding/binary" + "fmt" + + "github.com/srodi/ebpf-server/internal/core" + "github.com/srodi/ebpf-server/internal/events" + "github.com/srodi/ebpf-server/internal/programs" + "github.com/srodi/ebpf-server/pkg/logger" +) + +const ( + ProgramName = "your_monitor" + ProgramDescription = "Monitors your custom events" + ObjectPath = "bpf/your_monitor.o" + TracepointProgram = "trace_your_event" + EventsMapName = "events" +) + +type Program struct { + *programs.BaseProgram } -``` -**Response:** -```json -{ - "total_attempts": 5, - "pid": 1234, - "command": "", - "duration": 60, - "message": "Found 5 connection attempts from PID 1234 over 60 seconds" +func NewProgram() *Program { + base := programs.NewBaseProgram(ProgramName, ProgramDescription, ObjectPath) + return &Program{BaseProgram: base} } -``` - -#### GET /api/list-connections - -List all tracked connection events with optional query parameters. -**Query Parameters:** -- `pid` (integer, optional): Filter connections for specific Process ID -- `limit` (integer, optional): Maximum connections to return per PID (default: 100, max: 1000) +func (p *Program) Attach(ctx context.Context) error { + if !p.IsLoaded() { + return fmt.Errorf("program not loaded") + } + + logger.Debugf("Attaching %s program", ProgramName) + + if err := p.AttachTracepoint("syscalls", "your_event", TracepointProgram); err != nil { + return fmt.Errorf("failed to attach: %w", err) + } + + if err := p.StartEventProcessing(ctx, EventsMapName, p.parseEvent); err != nil { + return fmt.Errorf("failed to start processing: %w", err) + } + + p.SetAttached(true) + return nil +} -**Example:** -```bash -curl "http://localhost:8080/api/list-connections?pid=1234&limit=50" +func (p *Program) parseEvent(data []byte) (core.Event, error) { + if len(data) < 24 { + return nil, fmt.Errorf("insufficient data: %d bytes", len(data)) + } + + pid := binary.LittleEndian.Uint32(data[0:4]) + timestamp := binary.LittleEndian.Uint64(data[4:12]) + command := extractNullTerminatedString(data[12:]) + + metadata := map[string]interface{}{ + "custom_field": "custom_value", + } + + return events.NewBaseEvent(ProgramName, pid, command, timestamp, metadata), nil +} ``` +### 3. Register Your Program -#### POST /api/list-connections +Add to `internal/system/system.go` in the `Initialize()` method: -List all tracked connection events with JSON request body (alternative to GET). - -**Request Body:** -```json -{ - "pid": 1234, // Optional: Filter by PID - "limit": 100 // Optional: Limit results per PID +```go +// Register your program +yourProgram := your_monitor.NewProgram() +if err := s.manager.RegisterProgram(yourProgram); err != nil { + return fmt.Errorf("failed to register your_monitor: %w", err) } +logger.Debugf("✅ Registered your monitoring program") ``` -**Response (both GET and POST):** -```json -{ - "total_pids": 3, - "connections": { - "1234": [ - { - "pid": 1234, - "command": "curl", - "destination": "93.184.216.34:80", - "protocol": "TCP", - "return_code": 0, - "timestamp": "2025-07-31T14:30:56Z" - } - ] - }, - "truncated": false, - "message": "Found 1 total connections across 1 processes" +### 4. Create eBPF C Code + +Create `bpf/your_monitor.c`: + +```c +#include "vmlinux.h" +#include "bpf_helpers.h" +#include "bpf_tracing.h" + +struct your_event { + u32 pid; + u64 timestamp; + char comm[16]; + char custom_field[64]; +}; + +struct { + __uint(type, BPF_MAP_TYPE_RINGBUF); + __uint(max_entries, 256 * 1024); +} events SEC(".maps"); + +SEC("tracepoint/syscalls/your_event") +int trace_your_event(void *ctx) { + struct your_event *event; + + event = bpf_ringbuf_reserve(&events, sizeof(*event), 0); + if (!event) { + return 0; + } + + event->pid = bpf_get_current_pid_tgid() >> 32; + event->timestamp = bpf_ktime_get_ns(); + bpf_get_current_comm(&event->comm, sizeof(event->comm)); + + // Add your custom logic here + + bpf_ringbuf_submit(event, 0); + return 0; } -``` -#### GET / - -Service information and API documentation. - -**Response:** -```json -{ - "service": "eBPF Network Monitor", - "version": "v1.0.0", - "description": "HTTP API for eBPF-based network connection monitoring", - "endpoints": { - "POST /api/connection-summary": "Get connection summary for a process", - "GET|POST /api/list-connections": "List network connections", - "GET /health": "Service health check" - } -} +char LICENSE[] SEC("license") = "GPL"; ``` -### Example Usage - -**Start the server:** -```bash -# Build and run (requires root for eBPF) -sudo make run +## API Features -# Or run directly -sudo ./bin/ebpf-server -addr :8080 -``` +- **Unified Event API**: Single `/api/events` endpoint for all monitoring data +- **Flexible Filtering**: Filter by event type, PID, command, and time windows +- **Program Status**: View program status and metrics via `/api/programs` +- **Auto-Generated Documentation**: OpenAPI 3.0 spec from code annotations +- **Interactive Testing**: Built-in Swagger UI for API exploration -**Test the API:** -```bash -# Check health -curl http://localhost:8080/health +## API Endpoints -# Get connection summary for a specific command -curl -X POST http://localhost:8080/api/connection-summary \ - -H "Content-Type: application/json" \ - -d '{ - "command": "curl", - "duration": 30 - }' +### Core Endpoints -# List all connections -curl http://localhost:8080/api/list-connections +- **`GET /health`** - System health and status +- **`GET /api/events`** - Query events with filtering support +- **`GET /api/programs`** - List all programs and their status -# List connections for specific PID with limit -curl "http://localhost:8080/api/list-connections?pid=1234&limit=10" -``` +### Event Query Examples -**Integration with monitoring tools:** ```bash -# Use with Prometheus/monitoring -curl -s http://localhost:8080/api/connection-summary \ - -d '{"command":"nginx","duration":60}' | jq '.total_attempts' - -# Use with scripts -CONNECTIONS=$(curl -s http://localhost:8080/api/list-connections | jq '.total_pids') -echo "Currently tracking $CONNECTIONS processes" -``` +# Get all connection events from the last hour +curl "http://localhost:8080/api/events?type=connection&since=2023-01-01T00:00:00Z" -## Testing - -To test that the server is working correctly: - -1. **Run the unit tests:** - ```bash - make test - ``` - -2. **Test the HTTP API server:** - ```bash - # Start the server (requires root) - sudo make run - - # Test from another terminal - curl http://localhost:8080/health - - # Test connection summary - curl -X POST http://localhost:8080/api/connection-summary \ - -H "Content-Type: application/json" \ - -d '{"command":"test","duration":30}' - - # Test list connections - curl http://localhost:8080/api/list-connections - ``` - -3. **Generate network activity for monitoring:** - ```bash - # In another terminal, create some connections - curl -s http://httpbin.org/ip > /dev/null - curl -s https://www.google.com > /dev/null - - # Check captured connections - curl http://localhost:8080/api/list-connections | jq . - ``` - -4. **Test with specific processes:** - ```bash - # Get summary for curl commands - curl -X POST http://localhost:8080/api/connection-summary \ - -H "Content-Type: application/json" \ - -d '{"command":"curl","duration":60}' - - # Monitor specific PID - curl -X POST http://localhost:8080/api/connection-summary \ - -H "Content-Type: application/json" \ - -d '{"pid":1234,"duration":30}' - ``` - -**Note:** -- The API captures `connect()` syscall attempts, useful for monitoring **persistent services** and **connection patterns** -- For short-lived processes like individual curl commands, use the **command name** instead of PID -- Current eBPF program monitors TCP `connect()` syscalls only (not ICMP like ping) -- API responses include human-readable messages along with structured data - -## Protocol Detection and Testing - -The HTTP API server includes enhanced protocol detection that identifies connection types and provides detailed network information: - -### Supported Protocols - -**✅ Captured Protocols:** -- **TCP** - All TCP connections (HTTP, HTTPS, SSH, etc.) -- **UDP** - UDP connections that use `connect()` (DNS queries, some applications) -- **Unix Domain Sockets** - Local IPC connections - -**❌ Not Captured:** -- **UDP with sendto()** - Most UDP traffic uses `sendto()` without `connect()` -- **Raw sockets** - Don't use the `connect()` syscall -- **ICMP** - ping and other ICMP traffic - -### Protocol Detection Features - -- **Port-based heuristics** - Intelligently identifies protocols by destination port -- **Socket type classification** - Distinguishes STREAM (TCP) vs DGRAM (UDP) -- **Wall clock timestamps** - Human-readable time conversion from boot time -- **Complete connection details** - Source process, destination IP/port, protocol info - -### Testing Protocol Detection +# Get events for a specific process +curl "http://localhost:8080/api/events?pid=1234&limit=50" -```bash -# Start the HTTP API server -sudo make run - -# In another terminal, generate test connections -python3 -c " -import socket -import time - -print('Testing TCP connection...') -s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) -s.connect(('httpbin.org', 80)) -s.close() - -print('Testing UDP with connect()...') -s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) -s.connect(('8.8.8.8', 53)) # This will be captured -s.close() - -print('Testing UDP with sendto()...') -s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) -s.sendto(b'test', ('8.8.8.8', 53)) # This will NOT be captured -s.close() - -print('Test complete') -" - -# Check captured connections using the HTTP API -curl http://localhost:8080/api/list-connections | jq . - -# Get summary for python connections -curl -X POST http://localhost:8080/api/connection-summary \ - -H "Content-Type: application/json" \ - -d '{"command":"python3","duration":60}' -``` - -### Example Protocol Detection Output - -```json -{ - "pid": 1234, - "command": "python3", - "destination_ip": "8.8.8.8", - "destination_port": 53, - "destination": "8.8.8.8:53", - "address_family": 2, - "protocol": "UDP", - "socket_type": "DGRAM", - "wall_time": "2025-07-29T14:30:56Z" -} +# Get packet drop events with command filter +curl "http://localhost:8080/api/events?type=packet_drop&command=curl" ``` -### Connection Types You'll See +### Query Parameters -1. **DNS Queries** - `127.0.0.53:53` (local resolver) or `192.168.x.x:53` (network DNS) -2. **HTTP/HTTPS** - Various IPs on ports 80/443 with TCP protocol -3. **System Services** - Unix domain sockets with `address_family: 1` -4. **Application Traffic** - Protocol determined by port (SSH=22, HTTP=80, HTTPS=443, etc.) +- `type`: Event type filter (e.g., "connection", "packet_drop") +- `pid`: Process ID filter +- `command`: Command name filter +- `since`: RFC3339 timestamp for start time +- `until`: RFC3339 timestamp for end time +- `limit`: Maximum results (default: 100) ## Development -### Building - ```bash -# Build release version (production) -make build +# Development build with debug logging +make build-dev && sudo ./bin/ebpf-server-dev -# Build development version with debug symbols and verbose debug logging -make build-dev +# Generate API docs +make docs -# Build and run in development mode -make run-dev -``` - -**Debug Logging:** -- `make build`: Production build with minimal logging (INFO level and above) -- `make build-dev`: Development build with extensive debug logging showing: - - Raw eBPF event data and parsing details - - Connection tracking and storage operations - - API request processing and response details - - Ring buffer event processing information - -### Testing - -```bash -# Run all unit tests +# Run tests make test -# Run tests with race detection -make test-race - -# Test the HTTP API integration -curl http://localhost:8080/health -``` - -**Test Coverage:** -- **Logger Package**: Debug level functionality, global logger behavior, level switching -- **BPF Types**: Event parsing, IP conversion, protocol/socket type detection, time conversion -- **API Handlers**: HTTP endpoints, request/response handling, error cases, input validation -- **HTTP Server**: Route handling, timeouts, integration testing -- **Integration Testing**: API endpoint testing with various request types - -### Code Quality - -```bash -# Format code -make fmt - -# Run linters -make lint -``` - -### Development Setup - -```bash -# Setup development tools -make dev-setup +# Build eBPF programs +make build-bpf ``` ## Project Structure ``` -. -├── bpf/ # eBPF programs (C) -│ └── connection.c # Connection monitoring eBPF program -├── cmd/ -│ └── server/ -│ ├── main.go # Application entry point -│ └── debug.go # Debug build configuration +├── cmd/server/ # Main application entry point ├── internal/ -│ ├── api/ # HTTP API handlers and routes -│ │ ├── handlers.go # HTTP request handlers -│ │ └── handlers_test.go # API handler unit tests -│ └── bpf/ # eBPF program loader and utilities -│ ├── loader.go # eBPF program loading logic -│ ├── types.go # eBPF data structures -│ └── types_test.go # BPF types unit tests -├── pkg/ -│ └── logger/ # Custom logging package -│ ├── logger.go # Logger implementation -│ └── logger_test.go # Logger unit tests -├── go.mod # Go module definition -├── go.sum # Go module checksums -├── Makefile # Build automation -└── README.md # This file +│ ├── core/ # Core interfaces and types +│ ├── events/ # Event system (BaseEvent, streams) +│ ├── programs/ # eBPF program implementations +│ │ ├── base.go # BaseProgram foundation +│ │ ├── manager.go # Program manager +│ │ ├── connection/ # Network connection monitoring +│ │ └── packet_drop/ # Packet drop monitoring +│ ├── storage/ # Event storage and querying +│ ├── api/ # HTTP API handlers +│ └── system/ # System initialization and coordination +├── bpf/ # eBPF C programs and headers +├── docs/ # Documentation and API specs +└── pkg/logger/ # Logging utilities ``` -## Troubleshooting +## Requirements -### Common Issues +- **Linux kernel 4.18+** with eBPF support +- **Root privileges** for eBPF program loading +- **Dependencies**: Go 1.23+, Clang, libbpf-dev, kernel headers -1. **Permission Denied** - - eBPF programs require root privileges - - Run with `sudo` or as root user - -2. **eBPF Program Load Failed** - - Check kernel version (requires 4.18+) - - Ensure kernel headers are installed - - Verify BTF (BPF Type Format) support - -3. **Compilation Errors** - - Install clang and libbpf development packages - - Check that kernel headers match running kernel - -### Debug Mode - -Run with debug symbols and verbose logging for detailed troubleshooting: -```bash -make build-dev -sudo ./bin/ebpf-server-dev -``` - -Debug builds include detailed logging of: -- eBPF event processing and data parsing -- Ring buffer operations and raw event data -- Connection tracking and storage operations -- API request/response processing - -### Logs - -The server logs to stdout. Check for eBPF loading errors and HTTP server startup messages. - -## Contributing - -1. Fork the repository -2. Create a feature branch -3. Make your changes -4. Run tests and linting: `make test lint` -5. Commit your changes -6. Push to your fork -7. Create a Pull Request - -## Security Considerations - -- This server requires root privileges to load eBPF programs -- eBPF programs run in kernel space and should be thoroughly tested -- Network monitoring may capture sensitive information -- Consider running in a containerized environment for isolation +For detailed setup: [docs/setup.md](docs/setup.md) | Development guide: [docs/program-development.md](docs/program-development.md) ## License -This project is licensed under the MIT License - see the LICENSE file for details. - -## Related Projects - -- [Cilium eBPF Library](https://github.com/cilium/ebpf) - Go eBPF library used in this project -- [eBPF Documentation](https://ebpf.io/) - eBPF learning resources -- [BPFTrace](https://github.com/iovisor/bpftrace) - High-level tracing language for eBPF -- [Falco](https://falco.org/) - Cloud-native runtime security with eBPF - -## Support - -For questions and support: -- Create an issue on GitHub -- Check the troubleshooting section above -- Review eBPF and kernel documentation for system-specific issues +MIT License - see [LICENSE](LICENSE) file. diff --git a/bpf/connection.c b/bpf/connection.c index 4f25591..a3651fc 100644 --- a/bpf/connection.c +++ b/bpf/connection.c @@ -67,6 +67,8 @@ int trace_connect(struct trace_event_raw_sys_enter *ctx) { if (!e) return 0; e->pid = bpf_get_current_pid_tgid() >> 32; + // bpf_ktime_get_ns() returns nanoseconds since boot - this will be + // converted to wall-clock time in userspace using system boot time e->ts = bpf_ktime_get_ns(); e->ret = 0; // Initialize ret field bpf_get_current_comm(&e->comm, sizeof(e->comm)); diff --git a/bpf/packet_drop.c b/bpf/packet_drop.c new file mode 100644 index 0000000..7a49e45 --- /dev/null +++ b/bpf/packet_drop.c @@ -0,0 +1,100 @@ +#include +#include +#include +#include + +char LICENSE[] SEC("license") = "GPL"; + +// Packet drop event structure +struct drop_event_t { + u32 pid; // Process ID + u64 ts; // Timestamp (nanoseconds since boot) + char comm[16]; // Command name + u32 drop_reason; // Drop reason code + u32 skb_len; // Socket buffer length (when available) + u8 padding[8]; // Padding for alignment +} __attribute__((packed)); // Force no padding + +// Ring buffer for packet drop events +struct { + __uint(type, BPF_MAP_TYPE_RINGBUF); + __uint(max_entries, 256 * 1024); // 256KB ring buffer +} drop_events SEC(".maps"); + +// Tracepoint for kfree_skb (packet drops) +SEC("tracepoint/skb/kfree_skb") +int trace_kfree_skb(void *ctx) { + struct drop_event_t *event; + u64 pid_tgid; + u32 pid; + + // Get current task info + pid_tgid = bpf_get_current_pid_tgid(); + pid = pid_tgid >> 32; + + // Skip kernel threads (PID 0) + if (pid == 0) + return 0; + + // Reserve space in ring buffer + event = bpf_ringbuf_reserve(&drop_events, sizeof(*event), 0); + if (!event) + return 0; + + // Initialize event structure + __builtin_memset(event, 0, sizeof(*event)); + + // Fill basic event information + event->pid = pid; + // bpf_ktime_get_ns() returns nanoseconds since boot - this will be + // converted to wall-clock time in userspace using system boot time + event->ts = bpf_ktime_get_ns(); + event->drop_reason = 1; // Generic drop reason + event->skb_len = 1; // Mark as valid drop event + + // Get command name + bpf_get_current_comm(event->comm, sizeof(event->comm)); + + // Submit event to ring buffer + bpf_ringbuf_submit(event, 0); + return 0; +} + +// Alternative: Monitor failed socket operations +SEC("kprobe/tcp_drop") +int trace_tcp_drop(struct pt_regs *ctx) { + struct drop_event_t *event; + u64 pid_tgid; + u32 pid; + + // Get current task info + pid_tgid = bpf_get_current_pid_tgid(); + pid = pid_tgid >> 32; + + // Skip kernel threads + if (pid == 0) + return 0; + + // Reserve space in ring buffer + event = bpf_ringbuf_reserve(&drop_events, sizeof(*event), 0); + if (!event) + return 0; + + // Initialize event structure + __builtin_memset(event, 0, sizeof(*event)); + + // Fill basic event information + event->pid = pid; + // bpf_ktime_get_ns() returns nanoseconds since boot - this will be + // converted to wall-clock time in userspace using system boot time + event->ts = bpf_ktime_get_ns(); + event->drop_reason = 2; // TCP drop + event->skb_len = 1; // Mark as valid drop event + + // Get command name + bpf_get_current_comm(event->comm, sizeof(event->comm)); + + // Submit event to ring buffer + bpf_ringbuf_submit(event, 0); + return 0; +} diff --git a/bpf/packet_drop.o b/bpf/packet_drop.o new file mode 100644 index 0000000..de4bee5 Binary files /dev/null and b/bpf/packet_drop.o differ diff --git a/cmd/server/main.go b/cmd/server/main.go index 3cba604..10e0222 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -10,8 +10,11 @@ import ( "time" "github.com/srodi/ebpf-server/internal/api" - "github.com/srodi/ebpf-server/internal/bpf" + "github.com/srodi/ebpf-server/internal/system" "github.com/srodi/ebpf-server/pkg/logger" + + _ "github.com/srodi/ebpf-server/docs/swagger" // Import generated docs + httpSwagger "github.com/swaggo/http-swagger" ) func main() { @@ -21,9 +24,25 @@ func main() { ) flag.Parse() - // Load and attach eBPF programs - if err := bpf.LoadAndAttach(); err != nil { - logger.Fatalf("failed to load eBPF: %v", err) + // Check if debug logging is enabled + logger.Info("Starting eBPF Network Monitor...") + logger.Debug("Debug logging is enabled") + logger.Debugf("Debug logging test - IsDebugEnabled: %v", logger.IsDebugEnabled()) + + // Create a new system instance + ctx := context.Background() + logger.Debug("Creating system instance...") + system := system.NewSystem() + // Initialize API with the system + api.Initialize(system) + + // Initialize and start eBPF programs + if err := system.Initialize(); err != nil { + logger.Fatalf("failed to initialize eBPF: %v", err) + } + + if err := system.Start(ctx); err != nil { + logger.Fatalf("failed to start eBPF: %v", err) } // Setup signal handling for graceful shutdown @@ -33,8 +52,12 @@ func main() { go func() { <-c - logger.Info("Shutting down...") - bpf.Cleanup() + logger.Info("Shutdown signal received...") + logger.Debug("Starting cleanup process...") + if err := system.Stop(ctx); err != nil { + logger.Errorf("Error stopping eBPF system: %v", err) + } + logger.Debug("eBPF cleanup complete, canceling context...") cancel() }() @@ -43,9 +66,18 @@ func main() { // API endpoints mux.HandleFunc("/api/connection-summary", api.HandleConnectionSummary) + mux.HandleFunc("/api/packet-drop-summary", api.HandlePacketDropSummary) mux.HandleFunc("/api/list-connections", api.HandleListConnections) + mux.HandleFunc("/api/list-packet-drops", api.HandleListPacketDrops) mux.HandleFunc("/health", api.HandleHealth) + // New auto-generated API endpoints + mux.HandleFunc("/api/programs", api.HandlePrograms) + mux.HandleFunc("/api/events", api.HandleEvents) + + // Swagger documentation + mux.HandleFunc("/docs/", httpSwagger.WrapHandler) + // Root endpoint with service information mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/" { @@ -55,17 +87,27 @@ func main() { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - w.Write([]byte(`{ + if _, err := w.Write([]byte(`{ "service": "eBPF Network Monitor", "version": "v1.0.0", - "description": "HTTP API for eBPF-based network connection monitoring", + "description": "HTTP API for eBPF-based network connection and packet drop monitoring", "endpoints": { "POST /api/connection-summary": "Get connection summary for a process", + "POST /api/packet-drop-summary": "Get packet drop summary for a process", "GET|POST /api/list-connections": "List network connections", + "GET|POST /api/list-packet-drops": "List packet drops", + "GET /api/programs": "List active eBPF programs", + "GET /api/events": "Get filtered events", "GET /health": "Service health check" }, - "documentation": "See README.md for detailed API usage" - }`)) + "documentation": { + "api": "/docs/", + "swagger_json": "/docs/swagger.json", + "swagger_yaml": "/docs/swagger.yaml" + } + }`)); err != nil { + logger.Error("Failed to write health response", "error", err) + } }) logger.Infof("Starting eBPF Network Monitor HTTP API on %s...", *httpAddr) @@ -87,12 +129,15 @@ func main() { }() // Wait for context cancellation (shutdown signal) + logger.Debug("Waiting for shutdown signal...") <-ctx.Done() + logger.Debug("Context canceled, starting graceful shutdown...") // Graceful shutdown of HTTP server shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second) defer shutdownCancel() + logger.Debug("Shutting down HTTP server...") if err := httpServer.Shutdown(shutdownCtx); err != nil { logger.Fatalf("HTTP server shutdown error: %v", err) } diff --git a/cmd/server/main_test.go b/cmd/server/main_test.go index 9e310eb..e3570f9 100644 --- a/cmd/server/main_test.go +++ b/cmd/server/main_test.go @@ -9,8 +9,20 @@ import ( "time" "github.com/srodi/ebpf-server/internal/api" + "github.com/srodi/ebpf-server/internal/system" ) +// setupTestSystem creates a mock system for testing +func setupTestSystem() *system.System { + // Create a test system + testSystem := system.NewSystem() + + // Initialize the API with the test system + api.Initialize(testSystem) + + return testSystem +} + func TestHTTPServerSetup(t *testing.T) { // Create a test HTTP server with our routes mux := http.NewServeMux() @@ -29,11 +41,13 @@ func TestHTTPServerSetup(t *testing.T) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - w.Write([]byte(`{ + if _, err := w.Write([]byte(`{ "service": "eBPF Network Monitor", "version": "v1.0.0", "description": "HTTP API for eBPF-based network connection monitoring" - }`)) + }`)); err != nil { + http.Error(w, "Failed to write response", http.StatusInternalServerError) + } }) // Test server creation @@ -62,6 +76,9 @@ func TestHTTPServerSetup(t *testing.T) { } func TestHTTPHealthEndpoint(t *testing.T) { + // Setup test system + _ = setupTestSystem() + mux := http.NewServeMux() mux.HandleFunc("/health", api.HandleHealth) @@ -78,13 +95,13 @@ func TestHTTPHealthEndpoint(t *testing.T) { t.Errorf("expected status 200, got %d", resp.StatusCode) } - var healthResponse map[string]string + var healthResponse map[string]interface{} if err := json.NewDecoder(resp.Body).Decode(&healthResponse); err != nil { t.Fatalf("failed to decode JSON response: %v", err) } if healthResponse["status"] != "healthy" { - t.Errorf("expected status 'healthy', got '%s'", healthResponse["status"]) + t.Errorf("expected status 'healthy', got '%v'", healthResponse["status"]) } } @@ -112,24 +129,19 @@ func TestHTTPAPIEndpoints(t *testing.T) { } func TestHTTPConnectionSummaryValidation(t *testing.T) { + // Setup test system + _ = setupTestSystem() + mux := http.NewServeMux() mux.HandleFunc("/api/connection-summary", api.HandleConnectionSummary) server := httptest.NewServer(mux) defer server.Close() - // Test invalid request (missing duration) - reqData := map[string]interface{}{ - "pid": 1234, - // Missing duration - } - - reqBody, err := json.Marshal(reqData) - if err != nil { - t.Fatal(err) - } + // Test invalid JSON request + invalidJSON := `{"pid": "invalid_number", "duration_seconds": "not_a_number"` - resp, err := http.Post(server.URL+"/api/connection-summary", "application/json", bytes.NewBuffer(reqBody)) + resp, err := http.Post(server.URL+"/api/connection-summary", "application/json", bytes.NewBufferString(invalidJSON)) if err != nil { t.Fatalf("failed to make POST request: %v", err) } diff --git a/coverage.out b/coverage.out new file mode 100644 index 0000000..1d1a807 --- /dev/null +++ b/coverage.out @@ -0,0 +1,804 @@ +mode: atomic +github.com/srodi/ebpf-server/docs/swagger/docs.go:332.13,334.2 1 0 +github.com/srodi/ebpf-server/cmd/server/main.go:20.13,28.44 3 0 +github.com/srodi/ebpf-server/cmd/server/main.go:28.44,30.3 1 0 +github.com/srodi/ebpf-server/cmd/server/main.go:33.2,37.12 4 0 +github.com/srodi/ebpf-server/cmd/server/main.go:37.12,42.3 4 0 +github.com/srodi/ebpf-server/cmd/server/main.go:45.2,62.67 10 0 +github.com/srodi/ebpf-server/cmd/server/main.go:62.67,63.24 1 0 +github.com/srodi/ebpf-server/cmd/server/main.go:63.24,66.4 2 0 +github.com/srodi/ebpf-server/cmd/server/main.go:68.3,88.20 3 0 +github.com/srodi/ebpf-server/cmd/server/main.go:88.20,90.4 1 0 +github.com/srodi/ebpf-server/cmd/server/main.go:93.2,105.12 3 0 +github.com/srodi/ebpf-server/cmd/server/main.go:105.12,106.84 1 0 +github.com/srodi/ebpf-server/cmd/server/main.go:106.84,108.4 1 0 +github.com/srodi/ebpf-server/cmd/server/main.go:112.2,118.57 4 0 +github.com/srodi/ebpf-server/cmd/server/main.go:118.57,120.3 1 0 +github.com/srodi/ebpf-server/cmd/server/main.go:122.2,122.41 1 0 +github.com/srodi/ebpf-server/docs/swagger/docs.go:332.13,334.2 1 1 +github.com/srodi/ebpf-server/internal/bpf/base.go:27.37,29.2 1 14 +github.com/srodi/ebpf-server/internal/bpf/base.go:32.43,34.2 1 0 +github.com/srodi/ebpf-server/internal/bpf/base.go:37.41,40.53 2 8 +github.com/srodi/ebpf-server/internal/bpf/base.go:40.53,42.3 1 38 +github.com/srodi/ebpf-server/internal/bpf/base.go:43.2,43.20 1 8 +github.com/srodi/ebpf-server/internal/bpf/base.go:47.50,48.25 1 34 +github.com/srodi/ebpf-server/internal/bpf/base.go:48.25,50.3 1 0 +github.com/srodi/ebpf-server/internal/bpf/base.go:51.2,51.42 1 34 +github.com/srodi/ebpf-server/internal/bpf/base.go:55.36,56.25 1 13 +github.com/srodi/ebpf-server/internal/bpf/base.go:56.25,58.3 1 1 +github.com/srodi/ebpf-server/internal/bpf/base.go:59.2,59.17 1 13 +github.com/srodi/ebpf-server/internal/bpf/base.go:63.26,66.16 2 1 +github.com/srodi/ebpf-server/internal/bpf/base.go:66.16,70.3 3 1 +github.com/srodi/ebpf-server/internal/bpf/base.go:73.2,75.16 3 0 +github.com/srodi/ebpf-server/internal/bpf/base.go:75.16,79.3 3 0 +github.com/srodi/ebpf-server/internal/bpf/base.go:82.2,84.89 2 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:24.56,26.2 1 2 +github.com/srodi/ebpf-server/internal/bpf/loader.go:28.50,30.2 1 2 +github.com/srodi/ebpf-server/internal/bpf/loader.go:32.56,34.2 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:36.63,39.2 2 2 +github.com/srodi/ebpf-server/internal/bpf/loader.go:46.56,48.2 1 2 +github.com/srodi/ebpf-server/internal/bpf/loader.go:50.50,52.2 1 2 +github.com/srodi/ebpf-server/internal/bpf/loader.go:54.56,56.2 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:58.63,61.2 2 2 +github.com/srodi/ebpf-server/internal/bpf/loader.go:68.53,70.2 1 2 +github.com/srodi/ebpf-server/internal/bpf/loader.go:72.60,74.2 1 2 +github.com/srodi/ebpf-server/internal/bpf/loader.go:76.59,78.2 1 1 +github.com/srodi/ebpf-server/internal/bpf/loader.go:80.49,82.2 1 1 +github.com/srodi/ebpf-server/internal/bpf/loader.go:84.51,86.2 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:88.69,90.2 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:92.49,94.2 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:96.53,98.2 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:100.70,102.2 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:104.100,107.2 2 2 +github.com/srodi/ebpf-server/internal/bpf/loader.go:109.73,111.65 2 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:111.65,113.3 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:114.2,114.36 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:122.53,124.2 1 2 +github.com/srodi/ebpf-server/internal/bpf/loader.go:126.60,128.2 1 2 +github.com/srodi/ebpf-server/internal/bpf/loader.go:130.59,132.2 1 1 +github.com/srodi/ebpf-server/internal/bpf/loader.go:134.49,136.2 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:138.51,140.2 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:142.69,144.2 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:146.49,148.2 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:150.53,152.2 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:154.70,156.2 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:158.100,161.2 2 2 +github.com/srodi/ebpf-server/internal/bpf/loader.go:163.73,165.60 2 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:165.60,167.3 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:168.2,168.36 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:193.68,201.2 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:203.46,205.2 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:207.53,209.2 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:211.52,213.2 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:215.42,217.16 2 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:217.16,219.3 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:221.2,222.16 2 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:222.16,224.3 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:226.2,230.12 4 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:233.44,234.25 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:234.25,236.3 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:238.2,239.17 2 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:239.17,241.3 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:243.2,244.16 2 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:244.16,246.3 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:248.2,251.16 3 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:251.16,254.3 2 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:256.2,258.12 3 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:261.62,262.21 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:262.21,264.3 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:266.2,266.15 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:266.15,268.3 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:270.2,276.12 5 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:279.42,280.16 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:280.16,282.3 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:284.2,284.21 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:284.21,286.3 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:288.2,288.21 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:288.21,291.3 2 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:293.2,293.28 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:293.28,294.15 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:294.15,296.4 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:298.2,300.25 2 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:300.25,303.3 2 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:305.2,309.12 4 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:312.46,314.2 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:316.63,318.2 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:320.93,323.2 2 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:325.66,327.59 2 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:327.59,329.3 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:330.2,330.36 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:333.45,336.6 2 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:336.6,337.10 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:338.23,340.10 2 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:341.11,343.18 2 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:343.18,344.27 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:344.27,346.6 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:347.5,348.13 2 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:351.4,351.59 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:351.59,353.5 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:358.61,359.20 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:359.20,361.3 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:363.2,381.30 14 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:381.30,383.3 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:383.8,383.36 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:383.36,384.33 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:384.33,386.4 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:390.2,390.29 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:390.29,392.3 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:395.2,396.49 2 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:396.49,398.3 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:401.2,401.9 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:402.30,402.30 0 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:403.10,404.64 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:407.2,410.12 2 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:435.68,443.2 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:445.46,447.2 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:449.53,451.2 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:453.52,455.2 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:457.42,459.16 2 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:459.16,461.3 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:463.2,464.16 2 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:464.16,466.3 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:468.2,472.12 4 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:475.44,476.25 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:476.25,478.3 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:480.2,483.70 2 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:483.70,485.17 2 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:485.17,487.4 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:487.9,491.4 3 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:495.2,495.69 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:495.69,497.17 2 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:497.17,499.4 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:499.9,503.4 3 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:506.2,506.24 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:506.24,508.3 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:510.2,511.16 2 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:511.16,512.29 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:512.29,514.4 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:515.3,515.68 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:518.2,520.12 3 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:523.62,524.21 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:524.21,526.3 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:528.2,528.15 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:528.15,530.3 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:532.2,538.12 5 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:541.42,542.16 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:542.16,544.3 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:546.2,546.21 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:546.21,548.3 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:550.2,550.21 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:550.21,553.3 2 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:555.2,555.28 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:555.28,556.15 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:556.15,558.4 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:560.2,562.25 2 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:562.25,565.3 2 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:567.2,571.12 4 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:574.46,576.2 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:578.63,580.2 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:582.93,585.2 2 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:587.66,589.53 2 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:589.53,591.3 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:592.2,592.36 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:595.45,598.6 2 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:598.6,599.10 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:600.23,602.10 2 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:603.11,605.18 2 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:605.18,606.27 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:606.27,608.6 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:609.5,610.13 2 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:613.4,613.59 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:613.59,615.5 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:620.61,621.20 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:621.20,623.3 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:625.2,632.21 6 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:632.21,634.3 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:637.2,638.49 2 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:638.49,640.3 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:643.2,643.9 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:644.30,644.30 0 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:645.10,646.65 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:649.2,653.12 2 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:657.38,662.70 3 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:662.70,664.3 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:667.2,668.70 2 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:668.70,670.3 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:672.2,673.12 2 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:677.28,679.47 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:679.47,681.3 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:684.2,687.34 2 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:687.34,690.3 2 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:693.2,693.50 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:693.50,695.3 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:698.2,698.48 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:698.48,700.3 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:703.2,703.50 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:703.50,705.3 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:708.2,708.49 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:708.49,710.3 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:712.2,713.12 2 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:717.16,718.26 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:718.26,719.49 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:719.49,721.4 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:722.3,722.39 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:727.80,728.26 1 4 +github.com/srodi/ebpf-server/internal/bpf/loader.go:728.26,730.3 1 2 +github.com/srodi/ebpf-server/internal/bpf/loader.go:732.2,732.71 1 2 +github.com/srodi/ebpf-server/internal/bpf/loader.go:732.71,734.3 1 2 +github.com/srodi/ebpf-server/internal/bpf/loader.go:735.2,735.10 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:739.80,740.26 1 4 +github.com/srodi/ebpf-server/internal/bpf/loader.go:740.26,742.3 1 2 +github.com/srodi/ebpf-server/internal/bpf/loader.go:744.2,744.72 1 2 +github.com/srodi/ebpf-server/internal/bpf/loader.go:744.72,746.3 1 2 +github.com/srodi/ebpf-server/internal/bpf/loader.go:747.2,747.10 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:751.45,752.26 1 3 +github.com/srodi/ebpf-server/internal/bpf/loader.go:752.26,754.3 1 2 +github.com/srodi/ebpf-server/internal/bpf/loader.go:757.2,760.65 3 1 +github.com/srodi/ebpf-server/internal/bpf/loader.go:760.65,763.45 2 1 +github.com/srodi/ebpf-server/internal/bpf/loader.go:763.45,764.33 1 1 +github.com/srodi/ebpf-server/internal/bpf/loader.go:764.33,765.61 1 1 +github.com/srodi/ebpf-server/internal/bpf/loader.go:765.61,767.6 1 1 +github.com/srodi/ebpf-server/internal/bpf/loader.go:770.3,770.16 1 1 +github.com/srodi/ebpf-server/internal/bpf/loader.go:773.2,773.33 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:777.49,778.26 1 3 +github.com/srodi/ebpf-server/internal/bpf/loader.go:778.26,780.3 1 2 +github.com/srodi/ebpf-server/internal/bpf/loader.go:783.2,786.60 3 1 +github.com/srodi/ebpf-server/internal/bpf/loader.go:786.60,789.39 2 1 +github.com/srodi/ebpf-server/internal/bpf/loader.go:789.39,790.33 1 1 +github.com/srodi/ebpf-server/internal/bpf/loader.go:790.33,791.61 1 1 +github.com/srodi/ebpf-server/internal/bpf/loader.go:791.61,793.6 1 1 +github.com/srodi/ebpf-server/internal/bpf/loader.go:796.3,796.16 1 1 +github.com/srodi/ebpf-server/internal/bpf/loader.go:799.2,799.37 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:803.25,804.26 1 3 +github.com/srodi/ebpf-server/internal/bpf/loader.go:804.26,807.3 2 3 +github.com/srodi/ebpf-server/internal/bpf/loader.go:808.2,808.36 1 0 +github.com/srodi/ebpf-server/internal/bpf/loader.go:812.28,814.2 1 1 +github.com/srodi/ebpf-server/internal/bpf/manager.go:36.28,42.2 1 16 +github.com/srodi/ebpf-server/internal/bpf/manager.go:45.61,50.43 4 25 +github.com/srodi/ebpf-server/internal/bpf/manager.go:50.43,52.3 1 2 +github.com/srodi/ebpf-server/internal/bpf/manager.go:54.2,56.12 3 23 +github.com/srodi/ebpf-server/internal/bpf/manager.go:60.56,65.13 4 3 +github.com/srodi/ebpf-server/internal/bpf/manager.go:65.13,67.3 1 2 +github.com/srodi/ebpf-server/internal/bpf/manager.go:70.2,70.25 1 1 +github.com/srodi/ebpf-server/internal/bpf/manager.go:70.25,71.40 1 0 +github.com/srodi/ebpf-server/internal/bpf/manager.go:71.40,73.4 1 0 +github.com/srodi/ebpf-server/internal/bpf/manager.go:76.2,78.12 3 1 +github.com/srodi/ebpf-server/internal/bpf/manager.go:82.35,89.40 5 7 +github.com/srodi/ebpf-server/internal/bpf/manager.go:89.40,90.40 1 9 +github.com/srodi/ebpf-server/internal/bpf/manager.go:90.40,93.4 2 1 +github.com/srodi/ebpf-server/internal/bpf/manager.go:93.9,96.4 2 8 +github.com/srodi/ebpf-server/internal/bpf/manager.go:99.2,99.21 1 7 +github.com/srodi/ebpf-server/internal/bpf/manager.go:99.21,101.3 1 1 +github.com/srodi/ebpf-server/internal/bpf/manager.go:103.2,104.12 2 6 +github.com/srodi/ebpf-server/internal/bpf/manager.go:108.37,115.40 5 7 +github.com/srodi/ebpf-server/internal/bpf/manager.go:115.40,116.42 1 9 +github.com/srodi/ebpf-server/internal/bpf/manager.go:116.42,119.4 2 2 +github.com/srodi/ebpf-server/internal/bpf/manager.go:119.9,122.4 2 7 +github.com/srodi/ebpf-server/internal/bpf/manager.go:125.2,125.21 1 7 +github.com/srodi/ebpf-server/internal/bpf/manager.go:125.21,127.3 1 2 +github.com/srodi/ebpf-server/internal/bpf/manager.go:129.2,130.12 2 5 +github.com/srodi/ebpf-server/internal/bpf/manager.go:134.36,135.15 1 8 +github.com/srodi/ebpf-server/internal/bpf/manager.go:135.15,137.3 1 2 +github.com/srodi/ebpf-server/internal/bpf/manager.go:139.2,157.40 12 6 +github.com/srodi/ebpf-server/internal/bpf/manager.go:157.40,158.46 1 7 +github.com/srodi/ebpf-server/internal/bpf/manager.go:158.46,161.4 2 1 +github.com/srodi/ebpf-server/internal/bpf/manager.go:161.9,164.4 2 6 +github.com/srodi/ebpf-server/internal/bpf/manager.go:167.2,167.21 1 6 +github.com/srodi/ebpf-server/internal/bpf/manager.go:167.21,169.3 1 1 +github.com/srodi/ebpf-server/internal/bpf/manager.go:171.2,172.12 2 5 +github.com/srodi/ebpf-server/internal/bpf/manager.go:176.35,177.16 1 7 +github.com/srodi/ebpf-server/internal/bpf/manager.go:177.16,179.3 1 2 +github.com/srodi/ebpf-server/internal/bpf/manager.go:182.2,182.21 1 5 +github.com/srodi/ebpf-server/internal/bpf/manager.go:182.21,184.3 1 5 +github.com/srodi/ebpf-server/internal/bpf/manager.go:187.2,187.28 1 5 +github.com/srodi/ebpf-server/internal/bpf/manager.go:187.28,190.3 2 5 +github.com/srodi/ebpf-server/internal/bpf/manager.go:192.2,194.40 3 5 +github.com/srodi/ebpf-server/internal/bpf/manager.go:194.40,196.3 1 6 +github.com/srodi/ebpf-server/internal/bpf/manager.go:197.2,202.38 4 5 +github.com/srodi/ebpf-server/internal/bpf/manager.go:202.38,203.40 1 6 +github.com/srodi/ebpf-server/internal/bpf/manager.go:203.40,206.4 2 0 +github.com/srodi/ebpf-server/internal/bpf/manager.go:206.9,209.4 2 6 +github.com/srodi/ebpf-server/internal/bpf/manager.go:212.2,215.30 2 5 +github.com/srodi/ebpf-server/internal/bpf/manager.go:215.30,217.3 1 5 +github.com/srodi/ebpf-server/internal/bpf/manager.go:218.2,220.21 2 5 +github.com/srodi/ebpf-server/internal/bpf/manager.go:220.21,222.3 1 0 +github.com/srodi/ebpf-server/internal/bpf/manager.go:224.2,225.12 2 5 +github.com/srodi/ebpf-server/internal/bpf/manager.go:229.62,235.2 4 5 +github.com/srodi/ebpf-server/internal/bpf/manager.go:238.55,243.40 4 0 +github.com/srodi/ebpf-server/internal/bpf/manager.go:243.40,245.3 1 0 +github.com/srodi/ebpf-server/internal/bpf/manager.go:246.2,246.15 1 0 +github.com/srodi/ebpf-server/internal/bpf/manager.go:248.43,253.31 4 3 +github.com/srodi/ebpf-server/internal/bpf/manager.go:253.31,255.3 1 13 +github.com/srodi/ebpf-server/internal/bpf/manager.go:256.2,256.14 1 3 +github.com/srodi/ebpf-server/internal/bpf/manager.go:260.38,262.68 1 4 +github.com/srodi/ebpf-server/internal/bpf/manager.go:262.68,265.3 2 0 +github.com/srodi/ebpf-server/internal/bpf/manager.go:268.2,268.47 1 4 +github.com/srodi/ebpf-server/internal/bpf/manager.go:268.47,271.3 2 0 +github.com/srodi/ebpf-server/internal/bpf/manager.go:274.2,274.58 1 4 +github.com/srodi/ebpf-server/internal/bpf/manager.go:274.58,277.3 2 4 +github.com/srodi/ebpf-server/internal/bpf/manager.go:279.2,279.13 1 0 +github.com/srodi/ebpf-server/internal/bpf/manager.go:283.57,285.2 1 2 +github.com/srodi/ebpf-server/internal/bpf/manager.go:288.45,290.2 1 24 +github.com/srodi/ebpf-server/internal/bpf/manager.go:293.37,301.27 4 6 +github.com/srodi/ebpf-server/internal/bpf/manager.go:301.27,306.41 3 33 +github.com/srodi/ebpf-server/internal/bpf/manager.go:306.41,307.51 1 44 +github.com/srodi/ebpf-server/internal/bpf/manager.go:307.51,309.5 1 6 +github.com/srodi/ebpf-server/internal/bpf/manager.go:313.3,313.37 1 33 +github.com/srodi/ebpf-server/internal/bpf/manager.go:313.37,314.46 1 44 +github.com/srodi/ebpf-server/internal/bpf/manager.go:314.46,316.5 1 0 +github.com/srodi/ebpf-server/internal/bpf/manager.go:320.2,323.6 3 6 +github.com/srodi/ebpf-server/internal/bpf/manager.go:323.6,324.10 1 38 +github.com/srodi/ebpf-server/internal/bpf/manager.go:325.23,327.10 2 5 +github.com/srodi/ebpf-server/internal/bpf/manager.go:328.19,329.20 1 0 +github.com/srodi/ebpf-server/internal/bpf/manager.go:330.11,333.42 2 33 +github.com/srodi/ebpf-server/internal/bpf/manager.go:333.42,334.12 1 44 +github.com/srodi/ebpf-server/internal/bpf/manager.go:335.28,336.13 1 5 +github.com/srodi/ebpf-server/internal/bpf/manager.go:336.13,338.15 2 0 +github.com/srodi/ebpf-server/internal/bpf/manager.go:341.6,341.13 1 5 +github.com/srodi/ebpf-server/internal/bpf/manager.go:342.32,342.32 0 5 +github.com/srodi/ebpf-server/internal/bpf/manager.go:343.14,344.82 1 0 +github.com/srodi/ebpf-server/internal/bpf/manager.go:346.13,346.13 0 39 +github.com/srodi/ebpf-server/internal/bpf/manager.go:351.4,351.37 1 33 +github.com/srodi/ebpf-server/internal/bpf/manager.go:357.36,360.6 2 6 +github.com/srodi/ebpf-server/internal/bpf/manager.go:360.6,361.10 1 6 +github.com/srodi/ebpf-server/internal/bpf/manager.go:362.24,364.10 2 5 +github.com/srodi/ebpf-server/internal/bpf/manager.go:365.28,368.19 2 0 +github.com/srodi/ebpf-server/internal/bpf/manager.go:368.19,370.5 1 0 +github.com/srodi/ebpf-server/internal/bpf/storage.go:18.44,22.2 1 19 +github.com/srodi/ebpf-server/internal/bpf/storage.go:25.55,33.32 5 15 +github.com/srodi/ebpf-server/internal/bpf/storage.go:33.32,35.3 1 13 +github.com/srodi/ebpf-server/internal/bpf/storage.go:38.2,40.12 2 15 +github.com/srodi/ebpf-server/internal/bpf/storage.go:44.114,52.21 5 0 +github.com/srodi/ebpf-server/internal/bpf/storage.go:52.21,54.3 1 0 +github.com/srodi/ebpf-server/internal/bpf/storage.go:54.8,55.28 1 0 +github.com/srodi/ebpf-server/internal/bpf/storage.go:55.28,57.4 1 0 +github.com/srodi/ebpf-server/internal/bpf/storage.go:60.2,60.32 1 0 +github.com/srodi/ebpf-server/internal/bpf/storage.go:60.32,61.45 1 0 +github.com/srodi/ebpf-server/internal/bpf/storage.go:61.45,63.16 1 0 +github.com/srodi/ebpf-server/internal/bpf/storage.go:63.16,64.52 1 0 +github.com/srodi/ebpf-server/internal/bpf/storage.go:64.52,65.35 1 0 +github.com/srodi/ebpf-server/internal/bpf/storage.go:65.35,66.86 1 0 +github.com/srodi/ebpf-server/internal/bpf/storage.go:66.86,68.8 1 0 +github.com/srodi/ebpf-server/internal/bpf/storage.go:71.10,73.35 1 0 +github.com/srodi/ebpf-server/internal/bpf/storage.go:73.35,74.35 1 0 +github.com/srodi/ebpf-server/internal/bpf/storage.go:74.35,75.86 1 0 +github.com/srodi/ebpf-server/internal/bpf/storage.go:75.86,77.8 1 0 +github.com/srodi/ebpf-server/internal/bpf/storage.go:84.2,84.20 1 0 +github.com/srodi/ebpf-server/internal/bpf/storage.go:88.83,89.20 1 38 +github.com/srodi/ebpf-server/internal/bpf/storage.go:89.20,91.3 1 6 +github.com/srodi/ebpf-server/internal/bpf/storage.go:94.2,95.57 2 32 +github.com/srodi/ebpf-server/internal/bpf/storage.go:98.85,99.19 1 5 +github.com/srodi/ebpf-server/internal/bpf/storage.go:99.19,101.3 1 0 +github.com/srodi/ebpf-server/internal/bpf/storage.go:102.2,102.88 1 5 +github.com/srodi/ebpf-server/internal/bpf/storage.go:106.85,112.34 4 2 +github.com/srodi/ebpf-server/internal/bpf/storage.go:112.34,113.44 1 4 +github.com/srodi/ebpf-server/internal/bpf/storage.go:113.44,114.33 1 3 +github.com/srodi/ebpf-server/internal/bpf/storage.go:114.33,115.42 1 4 +github.com/srodi/ebpf-server/internal/bpf/storage.go:115.42,117.6 1 4 +github.com/srodi/ebpf-server/internal/bpf/storage.go:122.2,122.20 1 2 +github.com/srodi/ebpf-server/internal/bpf/storage.go:126.93,133.34 5 2 +github.com/srodi/ebpf-server/internal/bpf/storage.go:133.34,134.33 1 4 +github.com/srodi/ebpf-server/internal/bpf/storage.go:134.33,135.33 1 4 +github.com/srodi/ebpf-server/internal/bpf/storage.go:135.33,137.74 1 5 +github.com/srodi/ebpf-server/internal/bpf/storage.go:137.74,139.6 1 4 +github.com/srodi/ebpf-server/internal/bpf/storage.go:144.2,144.20 1 2 +github.com/srodi/ebpf-server/internal/bpf/storage.go:148.92,154.51 4 5 +github.com/srodi/ebpf-server/internal/bpf/storage.go:154.51,155.33 1 4 +github.com/srodi/ebpf-server/internal/bpf/storage.go:155.33,156.33 1 5 +github.com/srodi/ebpf-server/internal/bpf/storage.go:156.33,157.42 1 6 +github.com/srodi/ebpf-server/internal/bpf/storage.go:157.42,159.6 1 5 +github.com/srodi/ebpf-server/internal/bpf/storage.go:164.2,164.20 1 5 +github.com/srodi/ebpf-server/internal/bpf/storage.go:168.100,176.21 5 15 +github.com/srodi/ebpf-server/internal/bpf/storage.go:176.21,178.3 1 9 +github.com/srodi/ebpf-server/internal/bpf/storage.go:178.8,179.28 1 6 +github.com/srodi/ebpf-server/internal/bpf/storage.go:179.28,181.4 1 11 +github.com/srodi/ebpf-server/internal/bpf/storage.go:184.2,184.32 1 15 +github.com/srodi/ebpf-server/internal/bpf/storage.go:184.32,186.14 2 20 +github.com/srodi/ebpf-server/internal/bpf/storage.go:186.14,187.12 1 0 +github.com/srodi/ebpf-server/internal/bpf/storage.go:191.3,191.20 1 20 +github.com/srodi/ebpf-server/internal/bpf/storage.go:191.20,193.34 1 4 +github.com/srodi/ebpf-server/internal/bpf/storage.go:193.34,194.34 1 4 +github.com/srodi/ebpf-server/internal/bpf/storage.go:194.34,195.85 1 5 +github.com/srodi/ebpf-server/internal/bpf/storage.go:195.85,197.7 1 4 +github.com/srodi/ebpf-server/internal/bpf/storage.go:200.9,200.22 1 16 +github.com/srodi/ebpf-server/internal/bpf/storage.go:200.22,202.45 1 8 +github.com/srodi/ebpf-server/internal/bpf/storage.go:202.45,203.34 1 5 +github.com/srodi/ebpf-server/internal/bpf/storage.go:203.34,204.43 1 6 +github.com/srodi/ebpf-server/internal/bpf/storage.go:204.43,206.7 1 6 +github.com/srodi/ebpf-server/internal/bpf/storage.go:209.9,211.34 1 8 +github.com/srodi/ebpf-server/internal/bpf/storage.go:211.34,212.34 1 10 +github.com/srodi/ebpf-server/internal/bpf/storage.go:212.34,213.43 1 12 +github.com/srodi/ebpf-server/internal/bpf/storage.go:213.43,215.7 1 11 +github.com/srodi/ebpf-server/internal/bpf/storage.go:221.2,221.14 1 15 +github.com/srodi/ebpf-server/internal/bpf/storage.go:225.69,231.42 4 4 +github.com/srodi/ebpf-server/internal/bpf/storage.go:231.42,233.35 2 8 +github.com/srodi/ebpf-server/internal/bpf/storage.go:233.35,237.4 3 8 +github.com/srodi/ebpf-server/internal/bpf/storage.go:240.2,240.15 1 4 +github.com/srodi/ebpf-server/internal/bpf/storage.go:244.61,251.42 5 2 +github.com/srodi/ebpf-server/internal/bpf/storage.go:251.42,252.35 1 5 +github.com/srodi/ebpf-server/internal/bpf/storage.go:252.35,254.33 2 5 +github.com/srodi/ebpf-server/internal/bpf/storage.go:254.33,256.67 2 6 +github.com/srodi/ebpf-server/internal/bpf/storage.go:256.67,258.6 1 4 +github.com/srodi/ebpf-server/internal/bpf/storage.go:258.11,260.6 1 2 +github.com/srodi/ebpf-server/internal/bpf/storage.go:263.4,263.27 1 5 +github.com/srodi/ebpf-server/internal/bpf/storage.go:263.27,265.5 1 2 +github.com/srodi/ebpf-server/internal/bpf/storage.go:265.10,267.5 1 3 +github.com/srodi/ebpf-server/internal/bpf/storage.go:271.3,271.23 1 5 +github.com/srodi/ebpf-server/internal/bpf/storage.go:271.23,273.4 1 2 +github.com/srodi/ebpf-server/internal/bpf/storage.go:276.2,276.17 1 2 +github.com/srodi/ebpf-server/internal/bpf/storage.go:276.17,278.3 1 2 +github.com/srodi/ebpf-server/internal/bpf/storage.go:280.2,280.16 1 2 +github.com/srodi/ebpf-server/internal/bpf/types.go:37.37,40.53 2 4 +github.com/srodi/ebpf-server/internal/bpf/types.go:40.53,42.3 1 16 +github.com/srodi/ebpf-server/internal/bpf/types.go:43.2,43.20 1 4 +github.com/srodi/ebpf-server/internal/bpf/types.go:47.36,51.18 3 7 +github.com/srodi/ebpf-server/internal/bpf/types.go:52.15,53.22 1 4 +github.com/srodi/ebpf-server/internal/bpf/types.go:53.22,55.4 1 2 +github.com/srodi/ebpf-server/internal/bpf/types.go:57.3,58.21 2 2 +github.com/srodi/ebpf-server/internal/bpf/types.go:59.16,62.32 2 3 +github.com/srodi/ebpf-server/internal/bpf/types.go:62.32,63.14 1 18 +github.com/srodi/ebpf-server/internal/bpf/types.go:63.14,65.10 2 2 +github.com/srodi/ebpf-server/internal/bpf/types.go:68.3,68.14 1 3 +github.com/srodi/ebpf-server/internal/bpf/types.go:68.14,70.4 1 1 +github.com/srodi/ebpf-server/internal/bpf/types.go:71.3,72.21 2 2 +github.com/srodi/ebpf-server/internal/bpf/types.go:73.10,74.12 1 0 +github.com/srodi/ebpf-server/internal/bpf/types.go:79.41,83.14 3 3 +github.com/srodi/ebpf-server/internal/bpf/types.go:83.14,85.3 1 1 +github.com/srodi/ebpf-server/internal/bpf/types.go:88.2,88.26 1 2 +github.com/srodi/ebpf-server/internal/bpf/types.go:88.26,90.3 1 1 +github.com/srodi/ebpf-server/internal/bpf/types.go:92.2,92.45 1 1 +github.com/srodi/ebpf-server/internal/bpf/types.go:96.38,97.20 1 6 +github.com/srodi/ebpf-server/internal/bpf/types.go:98.9,99.15 1 1 +github.com/srodi/ebpf-server/internal/bpf/types.go:100.10,101.15 1 2 +github.com/srodi/ebpf-server/internal/bpf/types.go:102.10,103.19 1 3 +github.com/srodi/ebpf-server/internal/bpf/types.go:108.40,109.20 1 6 +github.com/srodi/ebpf-server/internal/bpf/types.go:110.9,111.18 1 1 +github.com/srodi/ebpf-server/internal/bpf/types.go:112.9,113.17 1 2 +github.com/srodi/ebpf-server/internal/bpf/types.go:114.10,115.19 1 3 +github.com/srodi/ebpf-server/internal/bpf/types.go:120.37,125.2 1 0 +github.com/srodi/ebpf-server/internal/bpf/types.go:128.46,133.2 3 1 +github.com/srodi/ebpf-server/internal/bpf/types.go:152.47,167.2 1 0 +github.com/srodi/ebpf-server/internal/bpf/types.go:172.41,175.53 2 3 +github.com/srodi/ebpf-server/internal/bpf/types.go:175.53,177.3 1 12 +github.com/srodi/ebpf-server/internal/bpf/types.go:178.2,178.20 1 3 +github.com/srodi/ebpf-server/internal/bpf/types.go:182.50,187.2 3 0 +github.com/srodi/ebpf-server/internal/bpf/types.go:190.50,191.22 1 0 +github.com/srodi/ebpf-server/internal/bpf/types.go:192.9,193.20 1 0 +github.com/srodi/ebpf-server/internal/bpf/types.go:194.9,195.20 1 0 +github.com/srodi/ebpf-server/internal/bpf/types.go:196.10,197.50 1 0 +github.com/srodi/ebpf-server/internal/bpf/types.go:214.51,225.2 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/event.go:20.39,22.2 1 2 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/event.go:25.46,26.22 1 5 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/event.go:27.9,28.20 1 3 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/event.go:29.9,30.20 1 1 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/event.go:31.10,32.50 1 1 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/event.go:49.47,60.2 1 1 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:39.52,47.2 1 1 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:50.36,52.2 1 1 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:55.43,57.2 1 1 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:60.42,62.2 1 1 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:65.32,67.16 2 0 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:67.16,69.3 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:71.2,72.16 2 0 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:72.16,74.3 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:76.2,80.12 4 0 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:84.34,85.25 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:85.25,87.3 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:89.2,92.70 2 0 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:92.70,94.17 2 0 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:94.17,96.4 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:96.9,100.4 3 0 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:104.2,104.69 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:104.69,106.17 2 0 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:106.17,108.4 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:108.9,112.4 3 0 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:115.2,115.24 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:115.24,117.3 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:120.2,121.16 2 0 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:121.16,122.29 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:122.29,124.4 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:125.3,125.68 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:128.2,130.12 3 0 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:134.52,135.21 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:135.21,137.3 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:139.2,139.15 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:139.15,141.3 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:143.2,150.12 5 0 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:154.32,155.16 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:155.16,157.3 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:160.2,160.21 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:160.21,162.3 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:165.2,165.21 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:165.21,168.3 2 0 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:171.2,171.28 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:171.28,172.15 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:172.15,174.4 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:176.2,179.25 2 0 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:179.25,182.3 2 0 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:184.2,188.12 4 0 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:192.36,194.2 1 1 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:197.57,199.2 1 1 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:202.83,206.2 2 2 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:209.60,211.53 2 1 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:211.53,213.3 1 1 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:214.2,214.40 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:218.35,221.6 2 0 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:221.6,222.10 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:223.23,225.10 2 0 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:226.11,228.18 2 0 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:228.18,229.27 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:229.27,231.6 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:232.5,233.13 2 0 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:236.4,236.59 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:236.59,238.5 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:244.51,245.20 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:245.20,247.3 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:249.2,265.21 6 0 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:265.21,267.3 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:270.2,270.48 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:270.48,272.3 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:275.2,275.9 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:276.29,276.29 0 0 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:277.10,278.65 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/packet_drop/program.go:281.2,285.12 2 0 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/event.go:26.39,28.2 1 2 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/event.go:31.36,35.18 3 6 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/event.go:36.15,37.22 1 4 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/event.go:37.22,39.4 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/event.go:41.3,42.21 2 4 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/event.go:43.16,46.32 2 2 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/event.go:46.32,47.14 1 32 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/event.go:47.14,49.10 2 2 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/event.go:52.3,52.14 1 2 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/event.go:52.14,54.4 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/event.go:55.3,56.21 2 2 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/event.go:57.10,58.12 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/event.go:63.41,67.14 3 3 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/event.go:67.14,69.3 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/event.go:72.2,72.26 1 3 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/event.go:72.26,74.3 1 1 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/event.go:76.2,76.45 1 2 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/event.go:80.38,81.20 1 4 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/event.go:82.9,83.15 1 2 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/event.go:84.10,85.15 1 1 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/event.go:86.10,87.19 1 1 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/event.go:92.40,93.20 1 4 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/event.go:94.9,95.18 1 1 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/event.go:96.9,97.17 1 1 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/event.go:98.10,99.19 1 2 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/event.go:120.47,135.2 1 1 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/program.go:39.52,47.2 1 1 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/program.go:50.36,52.2 1 1 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/program.go:55.43,57.2 1 1 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/program.go:60.42,62.2 1 1 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/program.go:65.32,67.16 2 0 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/program.go:67.16,69.3 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/program.go:71.2,72.16 2 0 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/program.go:72.16,74.3 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/program.go:76.2,80.12 4 0 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/program.go:84.34,85.25 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/program.go:85.25,87.3 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/program.go:90.2,91.17 2 0 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/program.go:91.17,93.3 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/program.go:95.2,96.16 2 0 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/program.go:96.16,98.3 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/program.go:100.2,104.16 3 0 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/program.go:104.16,107.3 2 0 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/program.go:109.2,111.12 3 0 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/program.go:115.52,116.21 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/program.go:116.21,118.3 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/program.go:120.2,120.15 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/program.go:120.15,122.3 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/program.go:124.2,131.12 5 0 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/program.go:135.32,136.16 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/program.go:136.16,138.3 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/program.go:141.2,141.21 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/program.go:141.21,143.3 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/program.go:146.2,146.21 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/program.go:146.21,149.3 2 0 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/program.go:152.2,152.28 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/program.go:152.28,153.15 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/program.go:153.15,155.4 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/program.go:157.2,160.25 2 0 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/program.go:160.25,163.3 2 0 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/program.go:165.2,169.12 4 0 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/program.go:173.36,175.2 1 1 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/program.go:178.57,180.2 1 1 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/program.go:183.83,187.2 2 1 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/program.go:190.60,192.59 2 1 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/program.go:192.59,194.3 1 1 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/program.go:195.2,195.40 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/program.go:199.35,202.6 2 0 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/program.go:202.6,203.10 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/program.go:204.23,206.10 2 0 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/program.go:207.11,209.18 2 0 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/program.go:209.18,210.27 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/program.go:210.27,212.6 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/program.go:213.5,214.13 2 0 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/program.go:217.4,217.59 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/program.go:217.59,219.5 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/program.go:225.51,226.20 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/program.go:226.20,228.3 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/program.go:230.2,261.30 14 0 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/program.go:261.30,264.3 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/program.go:264.8,264.36 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/program.go:264.36,266.33 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/program.go:266.33,268.4 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/program.go:272.2,272.29 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/program.go:272.29,274.3 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/program.go:277.2,277.48 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/program.go:277.48,279.3 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/program.go:282.2,282.9 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/program.go:283.29,283.29 0 0 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/program.go:284.10,285.64 1 0 +github.com/srodi/ebpf-server/internal/bpf/programs/connection/program.go:288.2,291.12 2 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:117.81,120.56 3 14 +github.com/srodi/ebpf-server/internal/api/handlers.go:120.56,122.3 1 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:126.80,131.2 1 7 +github.com/srodi/ebpf-server/internal/api/handlers.go:144.70,145.33 1 6 +github.com/srodi/ebpf-server/internal/api/handlers.go:145.33,148.3 2 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:150.2,151.61 2 6 +github.com/srodi/ebpf-server/internal/api/handlers.go:151.61,154.3 2 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:157.2,158.44 2 6 +github.com/srodi/ebpf-server/internal/api/handlers.go:158.44,160.3 1 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:163.2,163.22 1 6 +github.com/srodi/ebpf-server/internal/api/handlers.go:163.22,166.3 2 1 +github.com/srodi/ebpf-server/internal/api/handlers.go:167.2,167.24 1 5 +github.com/srodi/ebpf-server/internal/api/handlers.go:167.24,170.3 2 1 +github.com/srodi/ebpf-server/internal/api/handlers.go:171.2,171.35 1 4 +github.com/srodi/ebpf-server/internal/api/handlers.go:171.35,174.3 2 1 +github.com/srodi/ebpf-server/internal/api/handlers.go:175.2,175.35 1 3 +github.com/srodi/ebpf-server/internal/api/handlers.go:175.35,178.3 2 1 +github.com/srodi/ebpf-server/internal/api/handlers.go:181.2,185.19 4 2 +github.com/srodi/ebpf-server/internal/api/handlers.go:185.19,190.3 3 1 +github.com/srodi/ebpf-server/internal/api/handlers.go:190.8,195.3 3 1 +github.com/srodi/ebpf-server/internal/api/handlers.go:198.2,199.28 2 2 +github.com/srodi/ebpf-server/internal/api/handlers.go:199.28,202.3 1 1 +github.com/srodi/ebpf-server/internal/api/handlers.go:202.8,205.3 1 1 +github.com/srodi/ebpf-server/internal/api/handlers.go:207.2,215.47 2 2 +github.com/srodi/ebpf-server/internal/api/handlers.go:219.70,220.33 1 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:220.33,223.3 2 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:225.2,226.61 2 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:226.61,229.3 2 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:232.2,233.44 2 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:233.44,235.3 1 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:238.2,238.22 1 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:238.22,241.3 2 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:242.2,242.24 1 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:242.24,245.3 2 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:246.2,246.35 1 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:246.35,249.3 2 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:250.2,250.35 1 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:250.35,253.3 2 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:256.2,260.19 4 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:260.19,265.3 3 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:265.8,270.3 3 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:273.2,274.28 2 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:274.28,277.3 1 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:277.8,280.3 1 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:282.2,290.47 2 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:294.68,295.32 1 4 +github.com/srodi/ebpf-server/internal/api/handlers.go:295.32,298.3 1 2 +github.com/srodi/ebpf-server/internal/api/handlers.go:298.8,298.40 1 2 +github.com/srodi/ebpf-server/internal/api/handlers.go:298.40,301.3 1 2 +github.com/srodi/ebpf-server/internal/api/handlers.go:301.8,303.3 1 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:306.71,310.54 2 2 +github.com/srodi/ebpf-server/internal/api/handlers.go:310.54,311.51 1 1 +github.com/srodi/ebpf-server/internal/api/handlers.go:311.51,314.4 2 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:314.9,316.4 1 1 +github.com/srodi/ebpf-server/internal/api/handlers.go:319.2,319.60 1 2 +github.com/srodi/ebpf-server/internal/api/handlers.go:319.60,320.55 1 1 +github.com/srodi/ebpf-server/internal/api/handlers.go:320.55,323.4 2 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:323.9,325.4 1 1 +github.com/srodi/ebpf-server/internal/api/handlers.go:328.2,328.39 1 2 +github.com/srodi/ebpf-server/internal/api/handlers.go:331.72,333.61 2 2 +github.com/srodi/ebpf-server/internal/api/handlers.go:333.61,336.3 2 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:338.2,338.39 1 2 +github.com/srodi/ebpf-server/internal/api/handlers.go:341.87,343.43 1 4 +github.com/srodi/ebpf-server/internal/api/handlers.go:343.43,346.3 2 1 +github.com/srodi/ebpf-server/internal/api/handlers.go:348.2,349.22 2 3 +github.com/srodi/ebpf-server/internal/api/handlers.go:349.22,351.3 1 2 +github.com/srodi/ebpf-server/internal/api/handlers.go:354.2,366.52 4 3 +github.com/srodi/ebpf-server/internal/api/handlers.go:366.52,368.58 1 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:368.58,369.12 1 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:372.3,378.32 5 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:378.32,379.32 1 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:379.32,381.10 2 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:384.4,392.16 2 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:395.3,395.27 1 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:395.27,397.4 1 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:400.2,406.20 4 3 +github.com/srodi/ebpf-server/internal/api/handlers.go:406.20,408.44 2 1 +github.com/srodi/ebpf-server/internal/api/handlers.go:408.44,410.4 1 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:411.3,411.81 1 1 +github.com/srodi/ebpf-server/internal/api/handlers.go:412.8,414.44 2 2 +github.com/srodi/ebpf-server/internal/api/handlers.go:414.44,416.4 1 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:417.3,419.23 2 2 +github.com/srodi/ebpf-server/internal/api/handlers.go:419.23,421.4 1 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:424.2,433.47 3 3 +github.com/srodi/ebpf-server/internal/api/handlers.go:437.68,438.18 1 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:439.22,440.33 1 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:441.23,442.34 1 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:443.10,444.94 1 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:449.71,456.46 3 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:456.46,458.17 2 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:458.17,461.4 2 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:462.3,462.17 1 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:466.2,466.52 1 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:466.52,468.17 2 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:468.17,471.4 2 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:472.3,472.21 1 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:475.2,475.39 1 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:479.72,481.61 2 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:481.61,484.3 2 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:486.2,486.39 1 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:490.87,492.43 1 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:492.43,495.3 2 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:497.2,498.22 2 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:498.22,500.3 1 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:503.2,515.40 4 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:515.40,517.52 1 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:517.52,518.12 1 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:521.3,526.32 5 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:526.32,527.32 1 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:527.32,529.10 2 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:532.4,539.16 2 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:542.3,542.21 1 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:542.21,544.4 1 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:547.2,551.20 3 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:551.20,554.3 2 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:554.8,556.38 2 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:556.38,558.4 1 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:559.3,559.99 1 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:562.2,562.22 1 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:562.22,564.3 1 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:566.2,575.47 3 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:586.59,587.32 1 2 +github.com/srodi/ebpf-server/internal/api/handlers.go:587.32,590.3 2 1 +github.com/srodi/ebpf-server/internal/api/handlers.go:592.2,598.45 2 1 +github.com/srodi/ebpf-server/internal/api/handlers.go:609.61,610.32 1 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:610.32,613.3 2 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:615.2,616.20 2 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:616.20,619.3 2 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:621.2,622.48 2 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:622.48,624.60 2 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:624.60,625.37 1 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:625.37,627.5 1 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:630.3,635.5 1 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:638.2,644.47 2 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:661.59,662.32 1 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:662.32,665.3 2 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:668.2,675.18 7 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:675.18,676.67 1 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:676.67,679.4 2 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:679.9,681.4 1 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:684.2,685.23 2 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:685.23,686.81 1 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:686.81,689.4 2 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:689.9,691.4 1 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:694.2,695.20 2 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:695.20,696.72 1 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:696.72,699.4 2 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:699.9,701.4 1 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:704.2,705.20 2 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:705.20,708.3 2 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:710.2,714.16 4 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:714.16,717.3 2 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:720.2,720.25 1 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:720.25,722.3 1 0 +github.com/srodi/ebpf-server/internal/api/handlers.go:724.2,737.47 2 0 +github.com/srodi/ebpf-server/pkg/logger/logger.go:25.13,27.2 1 1 +github.com/srodi/ebpf-server/pkg/logger/logger.go:30.34,35.2 1 4 +github.com/srodi/ebpf-server/pkg/logger/logger.go:38.31,40.2 1 5 +github.com/srodi/ebpf-server/pkg/logger/logger.go:43.17,45.2 1 1 +github.com/srodi/ebpf-server/pkg/logger/logger.go:48.29,50.2 1 1 +github.com/srodi/ebpf-server/pkg/logger/logger.go:53.45,55.2 1 1 +github.com/srodi/ebpf-server/pkg/logger/logger.go:58.30,59.34 1 1 +github.com/srodi/ebpf-server/pkg/logger/logger.go:59.34,62.3 2 1 +github.com/srodi/ebpf-server/pkg/logger/logger.go:66.46,67.34 1 1 +github.com/srodi/ebpf-server/pkg/logger/logger.go:67.34,69.3 1 1 +github.com/srodi/ebpf-server/pkg/logger/logger.go:73.30,75.2 1 0 +github.com/srodi/ebpf-server/pkg/logger/logger.go:78.46,80.2 1 0 +github.com/srodi/ebpf-server/pkg/logger/logger.go:83.30,86.2 2 0 +github.com/srodi/ebpf-server/pkg/logger/logger.go:89.46,91.2 1 0 +github.com/srodi/ebpf-server/pkg/logger/logger.go:94.28,96.2 1 2 diff --git a/docs/program-development.md b/docs/program-development.md new file mode 100644 index 0000000..e4849f7 --- /dev/null +++ b/docs/program-development.md @@ -0,0 +1,1524 @@ +# eBPF Program Development Guide + +This guide explains how to develop new eBPF monitoring programs for the eBPF Network Monitor server. The server uses a modular architecture where each eBPF program operates independently and contributes events to a unified event stream and storage system. + +## Table of Contents + +- [Architecture Overview](#architecture-overview) +- [Program Structure](#program-structure) +- [Core Interfaces](#core-interfaces) +- [Step-by-Step Implementation](#step-by-step-implementation) +- [Event System](#event-system) +- [Manager Integration](#manager-integration) +- [API Integration](#api-integration) +- [Testing Your Program](#testing-your-program) +- [Best Practices](#best-practices) +- [Examples](#examples) + +## Architecture Overview + +The eBPF server follows a modular architecture with clear separation of concerns: + +``` +┌───────────────────────────────────────────────────────────┐ +│ System Layer │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ Manager │ │ +│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ +│ │ │ Connection │ │ Packet Drop │ │ Your New │ │ │ +│ │ │ Program │ │ Program │ │ Program │ │ │ +│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │ +│ └─────────────────────────────────────────────────────┘ │ +└───────────────────────────────────────────────────────────┘ + │ + ┌────────┴────────┐ + │ │ + ┌────────▼────────┐ ┌─────▼─────┐ + │ Event Storage │ │ HTTP API │ + │ (Unified) │ │ Handlers │ + └─────────────────┘ └───────────┘ +``` + +### Key Components + +1. **Core Interfaces**: Define contracts for Events, Programs, Managers, and Storage +2. **Event System**: Unified event creation, streaming, and storage +3. **Program Manager**: Coordinates program lifecycle and provides unified event streams +4. **Storage Layer**: Persistent event storage with query capabilities +5. **API Layer**: HTTP endpoints for querying events and program status +6. **System Layer**: Top-level coordination and initialization + +## Program Structure + +A typical eBPF monitoring program consists of: + +``` +internal/programs/your_program/ +├── your_program.go # Main program implementation +├── your_program_test.go # Unit tests +└── README.md # Program-specific documentation +``` + +Plus the eBPF C code: + +``` +bpf/ +├── your_program.c # eBPF kernel code +└── your_program.o # Compiled object (auto-generated) +``` + +## Core Interfaces + +### Event Interface + +All events must implement the `core.Event` interface: + +```go +type Event interface { + ID() string // Unique event identifier + Type() string // Event type (e.g., "connection") + PID() uint32 // Process ID + Command() string // Command name + Timestamp() uint64 // Kernel timestamp (nanoseconds) + Time() time.Time // Wall clock time + Metadata() map[string]interface{} // Event-specific data + json.Marshaler // JSON serialization +} +``` + +### Program Interface + +All eBPF programs must implement the `core.Program` interface: + +```go +type Program interface { + Name() string // Program name + Description() string // Human-readable description + Load(ctx context.Context) error // Load program into kernel + Attach(ctx context.Context) error // Attach to kernel hooks + Detach(ctx context.Context) error // Detach from kernel + IsLoaded() bool // Check if loaded + IsAttached() bool // Check if attached + EventStream() EventStream // Get event stream +} +``` +## Step-by-Step Implementation + +### 1. Create Directory Structure + +```bash +mkdir -p internal/programs/your_program +``` + +### 2. Implement Your Program + +Create `internal/programs/your_program/your_program.go`: + +```go +package your_program + +import ( + "context" + "fmt" + + "github.com/srodi/ebpf-server/internal/core" + "github.com/srodi/ebpf-server/internal/events" + "github.com/srodi/ebpf-server/internal/programs" + "github.com/srodi/ebpf-server/pkg/logger" +) + +const ( + // Program configuration + ProgramName = "your_program" + ProgramDescription = "Describe what your program monitors" + ObjectPath = "bpf/your_program.o" + + // eBPF program and map names (must match your C code) + TracepointProgram = "your_trace_function" + EventsMapName = "events" + + // Tracepoint/probe configuration + TracepointGroup = "syscalls" // or appropriate subsystem + TracepointName = "your_event" // specific tracepoint +) + +// Program implements your custom eBPF monitoring program. +type Program struct { + *programs.BaseProgram +} + +// NewProgram creates a new instance of your monitoring program. +func NewProgram() *Program { + base := programs.NewBaseProgram(ProgramName, ProgramDescription, ObjectPath) + return &Program{ + BaseProgram: base, + } +} + +// Attach attaches the program to the appropriate kernel hooks. +func (p *Program) Attach(ctx context.Context) error { + if !p.IsLoaded() { + return fmt.Errorf("program not loaded") + } + + logger.Debugf("Attaching %s monitoring program", ProgramName) + + // Attach to tracepoint (modify based on your hook type) + if err := p.AttachTracepoint(TracepointGroup, TracepointName, TracepointProgram); err != nil { + return fmt.Errorf("failed to attach tracepoint: %w", err) + } + + // Start event processing + if err := p.StartEventProcessing(ctx, EventsMapName, p.parseEvent); err != nil { + return fmt.Errorf("failed to start event processing: %w", err) + } + + p.SetAttached(true) + logger.Debugf("✅ %s program attached successfully", ProgramName) + return nil +} + +// parseEvent converts raw eBPF event data into structured events. +func (p *Program) parseEvent(data []byte) (core.Event, error) { + // Parse your event structure - this depends on your C struct + if len(data) < 24 { // Adjust based on your struct size + return nil, fmt.Errorf("event data too short: %d bytes", len(data)) + } + + // Example parsing (adjust to match your C struct): + pid := binary.LittleEndian.Uint32(data[0:4]) + timestamp := binary.LittleEndian.Uint64(data[4:12]) + // ... parse other fields + + // Extract command name + command := extractNullTerminatedString(data[12:]) + + // Create metadata with your specific fields + metadata := map[string]interface{}{ + "your_field1": "your_value1", + "your_field2": 42, + // Add your program-specific data + } + + return events.NewBaseEvent(ProgramName, pid, command, timestamp, metadata), nil +} + +// Utility function to extract null-terminated strings from binary data +func extractNullTerminatedString(data []byte) string { + for i, b := range data { + if b == 0 { + return string(data[:i]) + } + } + return string(data) +} +``` + +### 3. Create eBPF C Code + +Create `bpf/your_program.c`: + +```c +#include "vmlinux.h" +#include "bpf_helpers.h" +#include "bpf_tracing.h" +#include "bpf_core_read.h" + +// Event structure (must match your Go parsing) +struct your_event { + u32 pid; + u64 timestamp; + char comm[16]; + // Add your specific fields + u32 your_field1; + u64 your_field2; +}; + +// Ring buffer for sending events to userspace +struct { + __uint(type, BPF_MAP_TYPE_RINGBUF); + __uint(max_entries, 256 * 1024); +} events SEC(".maps"); + +SEC("tracepoint/syscalls/your_event") +int trace_your_event(struct trace_event_raw_sys_enter* ctx) { + struct your_event *event; + struct task_struct *task; + + // Reserve space in ring buffer + event = bpf_ringbuf_reserve(&events, sizeof(*event), 0); + if (!event) { + return 0; + } + + // Get current task + task = (struct task_struct*)bpf_get_current_task(); + + // Fill event data + event->pid = bpf_get_current_pid_tgid() >> 32; + event->timestamp = bpf_ktime_get_ns(); + bpf_get_current_comm(&event->comm, sizeof(event->comm)); + + // Add your specific logic here + event->your_field1 = /* your logic */; + event->your_field2 = /* your logic */; + + // Submit event + bpf_ringbuf_submit(event, 0); + return 0; +} + +char LICENSE[] SEC("license") = "GPL"; +``` + +### 4. Register Your Program + +Add your program to the system initialization in `internal/system/system.go`: + +```go +// In the Initialize() method: +yourProgram := your_program.NewProgram() +if err := s.manager.RegisterProgram(yourProgram); err != nil { + return fmt.Errorf("failed to register your program: %w", err) +} +logger.Debugf("✅ Registered your monitoring program") +``` + +### 5. Update Build System + +Add your eBPF program to the Makefile: + +```makefile +# Add to BPF_SOURCES +BPF_SOURCES := $(wildcard bpf/*.c) +``` + +The existing build system will automatically compile your `.c` file to `.o`. + Timestamp uint64 `json:"timestamp_ns"` + Command string `json:"command"` + CustomField1 uint32 `json:"custom_field1"` + CustomField2 string `json:"custom_field2"` + WallTime string `json:"wall_time"` + Note string `json:"note"` +} + +func (e *Event) MarshalJSON() ([]byte, error) { + return json.Marshal(EventJSON{ + PID: e.GetPID(), + Timestamp: e.GetTimestamp(), + Command: e.GetCommand(), + CustomField1: e.CustomField1, + CustomField2: e.CustomField2, + WallTime: e.GetWallClockTime().Format(time.RFC3339), + Note: "timestamp_ns is nanoseconds since boot, wall_time is converted to UTC", + }) +} +``` + +### 3. Implement Program Interface + +Create `internal/bpf/programs/your_program/program.go`: + +```go +package your_program + +import ( + "context" + "fmt" + "github.com/cilium/ebpf" + "github.com/cilium/ebpf/link" + "github.com/cilium/ebpf/ringbuf" + "github.com/srodi/ebpf-server/internal/bpf" + "github.com/srodi/ebpf-server/pkg/logger" +) + +type Program struct { + name string + description string + objectPath string + + // eBPF resources + collection *ebpf.Collection + eventsMap *ebpf.Map + links []link.Link + reader *ringbuf.Reader + + // Event processing + eventChan chan bpf.BPFEvent + ctx context.Context + cancel context.CancelFunc + running bool + + // Storage + storage bpf.EventStorage +} + +func NewProgram(storage bpf.EventStorage) *Program { + return &Program{ + name: "your_program", + description: "Describe what your program monitors", + objectPath: "bpf/your_program.o", + eventChan: make(chan bpf.BPFEvent, 1000), + storage: storage, + } +} + +// Interface implementation +func (p *Program) GetName() string { return p.name } +func (p *Program) GetDescription() string { return p.description } +func (p *Program) GetObjectPath() string { return p.objectPath } + +func (p *Program) Load() error { + spec, err := ebpf.LoadCollectionSpec(p.objectPath) + if err != nil { + return fmt.Errorf("failed to load collection spec: %w", err) + } + + collection, err := ebpf.NewCollection(spec) + if err != nil { + return fmt.Errorf("failed to create collection: %w", err) + } + + p.collection = collection + p.eventsMap = collection.Maps["your_events_map"] + return nil +} + +func (p *Program) Attach() error { + // Attach to appropriate kernel hooks + // Example for tracepoint: + l, err := link.Tracepoint(link.TracepointOptions{ + Group: "your_subsystem", + Name: "your_tracepoint", + Program: p.collection.Programs["your_program_function"], + }) + if err != nil { + return fmt.Errorf("failed to attach tracepoint: %w", err) + } + p.links = append(p.links, l) + return nil +} + +func (p *Program) Start(ctx context.Context) error { + p.ctx, p.cancel = context.WithCancel(ctx) + + reader, err := ringbuf.NewReader(p.eventsMap) + if err != nil { + return fmt.Errorf("failed to create ring buffer reader: %w", err) + } + p.reader = reader + + p.running = true + go p.processEvents() + + return nil +} + +func (p *Program) Stop() error { + if p.cancel != nil { + p.cancel() + } + + if p.reader != nil { + p.reader.Close() + } + + for _, l := range p.links { + l.Close() + } + + if p.collection != nil { + p.collection.Close() + } + + p.running = false + return nil +} + +func (p *Program) IsRunning() bool { return p.running } + +func (p *Program) GetEventChannel() <-chan bpf.BPFEvent { + return p.eventChan +} + +func (p *Program) GetSummary(pid uint32, command string, durationSeconds int) int { + // Implement summary logic using storage + since := bpf.GetSystemBootTime().Add(-time.Duration(durationSeconds) * time.Second) + events, _ := p.storage.Get(pid, command, p.name, since) + return len(events) +} + +func (p *Program) GetAllEvents() map[uint32][]bpf.BPFEvent { + // Return all events from storage + allEvents := p.storage.GetAll() + if events, exists := allEvents[p.name]; exists { + return events + } + return make(map[uint32][]bpf.BPFEvent) +} + +// Private methods +func (p *Program) processEvents() { + logger.Info("Starting your_program event processing...") + + for { + select { + case <-p.ctx.Done(): + logger.Info("Stopping your_program event processing...") + return + default: + record, err := p.reader.Read() + if err != nil { + if p.ctx.Err() != nil { + return + } + logger.Errorf("Error reading from your_program ring buffer: %v", err) + continue + } + + if err := p.processEvent(record.RawSample); err != nil { + logger.Errorf("Error processing your_program event: %v", err) + } + } + } +} + +func (p *Program) processEvent(data []byte) error { + // Parse the raw event data from eBPF + // This depends on your C struct layout + + event := &Event{ + BaseEvent: bpf.BaseEvent{ + // Parse fields from data... + }, + // Parse custom fields... + } + + // Store event + if err := p.storage.Store(ctx, event); err != nil { + logger.Errorf("Failed to store event: %v", err) + return err + } + + // Send to channel for real-time processing + select { + case p.eventChan <- event: + default: + logger.Warn("Event channel full, dropping event") + } + + return nil +} +``` + +### 4. Write eBPF C Code + +Create `bpf/your_program.c`: + +```c +#include +#include +#include +#include + +char LICENSE[] SEC("license") = "GPL"; + +// Event structure (must match Go struct layout) +struct your_event_t { + u32 pid; + u64 ts; + char comm[16]; + u32 custom_field1; + char custom_field2[64]; + u8 padding[8]; // Ensure alignment +} __attribute__((packed)); + +// Ring buffer for events +struct { + __uint(type, BPF_MAP_TYPE_RINGBUF); + __uint(max_entries, 256 * 1024); +} your_events_map SEC(".maps"); + +// Your eBPF program +SEC("tracepoint/your_subsystem/your_tracepoint") +int your_program_function(void *ctx) { + struct your_event_t *event; + u64 pid_tgid; + u32 pid; + + pid_tgid = bpf_get_current_pid_tgid(); + pid = pid_tgid >> 32; + + // Skip kernel threads + if (pid == 0) + return 0; + + // Reserve space in ring buffer + event = bpf_ringbuf_reserve(&your_events_map, sizeof(*event), 0); + if (!event) + return 0; + + // Initialize event + __builtin_memset(event, 0, sizeof(*event)); + + // Fill event data + event->pid = pid; + event->ts = bpf_ktime_get_ns(); + bpf_get_current_comm(event->comm, sizeof(event->comm)); + + // Add your custom logic here + event->custom_field1 = /* your logic */; + // event->custom_field2 = /* your logic */; + + // Submit event + bpf_ringbuf_submit(event, 0); + return 0; +} +``` + +### 5. Register Your Program + +Add to `internal/bpf/loader.go` in the `registerDefaultPrograms()` function: + +```go +// Register your program +yourProgram := your_program.NewProgram(storage) +if err := globalManager.RegisterProgram(yourProgram); err != nil { + return fmt.Errorf("failed to register your_program: %w", err) +} +``` + +### 6. Build and Test + +```bash +# Compile eBPF code +make build-bpf + +# Run tests +go test ./internal/bpf/programs/your_program/... + +# Build and test the server +make build +sudo ./bin/ebpf-server +``` + +## Interface Requirements + +### BPFProgram Interface + +All programs must implement: + +```go +type BPFProgram interface { + GetName() string + GetDescription() string + GetObjectPath() string + Load() error + Attach() error + Start(ctx context.Context) error + Stop() error + IsRunning() bool + GetEventChannel() <-chan BPFEvent + GetSummary(pid uint32, command string, durationSeconds int) int + GetAllEvents() map[uint32][]BPFEvent +} +``` + +### BPFEvent Interface + +All events must implement: + +```go +type BPFEvent interface { + GetPID() uint32 + GetTimestamp() uint64 + GetCommand() string + GetEventType() string + GetWallClockTime() time.Time +} +``` + +## Event System + +The event system provides a unified way to handle events across all eBPF programs: + +### BaseEvent Structure + +Use the `events.BaseEvent` for most cases: + +```go +// Create an event using the events package +event := events.NewBaseEvent( + "your_program", // event type + pid, // process ID + command, // command name + timestamp, // kernel timestamp + metadata, // map[string]interface{} with custom data +) +``` + +### Timestamp Handling + +eBPF programs use `bpf_ktime_get_ns()` to capture timestamps, which returns nanoseconds since system boot. The event system automatically converts these to wall-clock time: + +**In eBPF C code:** +```c +// Use bpf_ktime_get_ns() - returns nanoseconds since boot +event->ts = bpf_ktime_get_ns(); +``` + +**In userspace Go code:** +```go +// The timestamp is automatically converted to wall-clock time +event := events.NewBaseEvent("connection", pid, command, timestamp, metadata) +// event.Time() returns proper wall-clock time +// event.Timestamp() returns original eBPF timestamp (nanoseconds since boot) +``` + +**Conversion Process:** +1. eBPF program captures `bpf_ktime_get_ns()` (nanoseconds since boot) +2. Userspace reads system boot time from `/proc/stat` (cached for performance) +3. Wall-clock time = boot_time + ebpf_timestamp +4. Both original timestamp and converted time are available in the event + +**Important Notes:** +- Always use `bpf_ktime_get_ns()` in eBPF programs for consistency +- Don't use `bpf_ktime_get_boot_ns()` or other time functions +- The conversion handles timezone and system clock adjustments +- Boot time is cached but can be reset with `events.ResetBootTimeCache()` +- On Linux: reads actual boot time from `/proc/stat` for accuracy +- On other platforms: uses fallback method suitable for development/testing +``` + +### Custom Events + +For complex events, you can implement the `core.Event` interface directly: + +```go +type CustomEvent struct { + id string + pid uint32 + timestamp uint64 + // your custom fields +} + +func (e *CustomEvent) ID() string { return e.id } +func (e *CustomEvent) Type() string { return "your_program" } +func (e *CustomEvent) PID() uint32 { return e.pid } +// ... implement other required methods +``` + +### Event Streaming + +Events flow through this pipeline: + +``` +eBPF Program → Ring Buffer → Event Parser → Event Stream → Storage/API +``` + +1. **eBPF Program**: Writes events to ring buffer +2. **Event Parser**: Converts binary data to Go structs +3. **Event Stream**: Provides channel-based event delivery +4. **Storage**: Persists events for querying +5. **API**: Exposes events via HTTP endpoints + +## Manager Integration + +The Manager coordinates all programs and provides unified interfaces: + +### Program Registration + +```go +// Programs are registered during system initialization +manager := programs.NewManager() + +// Register your program +program := your_program.NewProgram() +if err := manager.RegisterProgram(program); err != nil { + return fmt.Errorf("failed to register program: %w", err) +} + +// Lifecycle management +ctx := context.Background() +if err := manager.LoadAll(ctx); err != nil { + return fmt.Errorf("failed to load programs: %w", err) +} + +if err := manager.AttachAll(ctx); err != nil { + return fmt.Errorf("failed to attach programs: %w", err) +} + +// Get unified event stream +eventStream := manager.EventStream() +for event := range eventStream.Events() { + // Process events from all programs +} +``` + +### Program Status + +The manager provides program status information: + +```go +type ProgramStatus struct { + Name string `json:"name"` + Description string `json:"description"` + Loaded bool `json:"loaded"` + Attached bool `json:"attached"` + EventCount int64 `json:"event_count"` +} + +// Get status of all programs +statuses := manager.GetProgramStatus() +``` + +## Event Storage Integration + +Events are automatically stored in the unified storage system. The storage interface provides: + +### Storage Operations + +```go +type EventSink interface { + // Store saves an event + Store(ctx context.Context, event Event) error + + // Query retrieves events matching the given criteria + Query(ctx context.Context, query Query) ([]Event, error) + + // Count returns the number of events matching the criteria + Count(ctx context.Context, query Query) (int, error) +} +``` + +### Query Structure + +```go +type Query struct { + EventType string // Filter by event type (e.g., "your_program") + PID uint32 // Filter by process ID (0 = no filter) + Command string // Filter by command name + Since time.Time // Events after this time + Until time.Time // Events before this time + Limit int // Maximum number of results (0 = no limit) +} +``` + +### Usage Examples + +```go +// Query all events from your program +query := core.Query{ + EventType: "your_program", + Since: time.Now().Add(-1 * time.Hour), + Limit: 100, +} + +events, err := storage.Query(ctx, query) + +// Count events for specific PID +query := core.Query{ + EventType: "your_program", + PID: 1234, +} + +count, err := storage.Count(ctx, query) +``` + +## API Integration + +The HTTP API provides automatic endpoints for all events: + +### Built-in Endpoints + +All programs automatically get access to: + +- `GET /api/events` - Query events with comprehensive filtering +- `GET /api/programs` - List all programs and their status +- `GET /health` - System health and status +- `GET /api/list-connections` - Connection events (from connection program) +- `GET /api/list-packet-drops` - Packet drop events (from packet_drop program) + +### Event Query Parameters + +The `/api/events` endpoint supports: + +``` +GET /api/events?type=your_program&pid=1234&command=curl&since=2023-01-01T00:00:00Z&limit=100 +``` + +Parameters: +- `type`: Event type filter +- `pid`: Process ID filter +- `command`: Command name filter +- `since`: RFC3339 timestamp for start time +- `until`: RFC3339 timestamp for end time +- `limit`: Maximum results (default: 100) + +### Program Status API + +``` +GET /api/programs +``` + +Returns: +```json +{ + "programs": [ + { + "name": "your_program", + "description": "Your program description", + "loaded": true, + "attached": true, + "event_count": 1234 + } + ], + "total_programs": 3, + "running_programs": 2 +} +``` + +### Custom Endpoints (Optional) + +Add custom endpoints in `internal/api/handlers.go`: + +```go +// @Summary Get your program statistics +// @Description Returns metrics for your custom monitoring program +// @Tags your_program +// @Accept json +// @Produce json +// @Param pid query int false "Process ID filter" +// @Param duration_seconds query int false "Time window in seconds" +// @Success 200 {object} YourProgramResponse +// @Failure 400 {object} ErrorResponse +// @Router /api/your_program/summary [get] +func HandleYourProgramSummary(w http.ResponseWriter, r *http.Request) { + if globalSystem == nil { + http.Error(w, "System not initialized", http.StatusServiceUnavailable) + return + } + + // Parse query parameters + var pid uint32 + if pidStr := r.URL.Query().Get("pid"); pidStr != "" { + if pidVal, err := strconv.ParseUint(pidStr, 10, 32); err == nil { + pid = uint32(pidVal) + } + } + + duration := 60 // default + if durationStr := r.URL.Query().Get("duration_seconds"); durationStr != "" { + if durationVal, err := strconv.Atoi(durationStr); err == nil { + duration = durationVal + } + } + + // Build query for your program + query := core.Query{ + EventType: "your_program", + PID: pid, + Since: time.Now().Add(-time.Duration(duration) * time.Second), + } + + // Get event count + ctx := context.Background() + count, err := globalSystem.CountEvents(ctx, query) + if err != nil { + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + + // Return response + response := map[string]interface{}{ + "count": count, + "pid": pid, + "duration_seconds": duration, + "query_time": time.Now().Format(time.RFC3339), + } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(response); err != nil { + http.Error(w, "Internal server error", http.StatusInternalServerError) + } +} +``` + +Register your custom endpoint in `cmd/server/main.go`: + +```go +// Add to the HTTP routes setup +mux.HandleFunc("/api/your_program/summary", api.HandleYourProgramSummary) +``` + +## Testing Your Program + +Create comprehensive tests for your program: + +### 1. Unit Tests + +Create `internal/programs/your_program/your_program_test.go`: + +```go +package your_program + +import ( + "context" + "testing" + + "github.com/srodi/ebpf-server/internal/events" +) + +func TestProgram(t *testing.T) { + program := NewProgram() + + // Test basic properties + if program.Name() != ProgramName { + t.Errorf("Expected name %s, got %s", ProgramName, program.Name()) + } + + if program.Description() != ProgramDescription { + t.Errorf("Expected description %s, got %s", ProgramDescription, program.Description()) + } + + // Test initial state + if program.IsLoaded() { + t.Error("Program should not be loaded initially") + } + + if program.IsAttached() { + t.Error("Program should not be attached initially") + } +} + +func TestEventParser(t *testing.T) { + program := NewProgram() + + // Create mock event data (adjust based on your C struct) + eventData := make([]byte, 24) + // Fill with test data matching your struct + // pid (4 bytes) + binary.LittleEndian.PutUint32(eventData[0:4], 1234) + // timestamp (8 bytes) + binary.LittleEndian.PutUint64(eventData[4:12], uint64(time.Now().UnixNano())) + // command (12 bytes) + copy(eventData[12:], []byte("test_cmd\x00")) + + event, err := program.parseEvent(eventData) + if err != nil { + t.Fatalf("Failed to parse event: %v", err) + } + + if event.Type() != ProgramName { + t.Errorf("Expected event type %s, got %s", ProgramName, event.Type()) + } + + if event.PID() != 1234 { + t.Errorf("Expected PID 1234, got %d", event.PID()) + } + + if event.Command() != "test_cmd" { + t.Errorf("Expected command 'test_cmd', got '%s'", event.Command()) + } +} + +func TestParseInvalidData(t *testing.T) { + program := NewProgram() + + // Test with insufficient data + _, err := program.parseEvent([]byte{1, 2, 3}) + if err == nil { + t.Error("Expected error for insufficient data") + } +} +``` + +### 2. Integration Tests + +```go +func TestProgramIntegration(t *testing.T) { + // Skip integration tests in unit test mode + if testing.Short() { + t.Skip("Skipping integration test in short mode") + } + + ctx := context.Background() + program := NewProgram() + + // Test loading (requires eBPF object file) + if err := program.Load(ctx); err != nil { + t.Skipf("Cannot load program (missing object file): %v", err) + } + + if !program.IsLoaded() { + t.Error("Program should be loaded after Load()") + } + + // Cleanup + if err := program.Detach(ctx); err != nil { + t.Errorf("Failed to detach program: %v", err) + } +} +``` + +### 3. Mock Testing + +For testing without eBPF: + +```go +type MockProgram struct { + name string + loaded bool + attached bool + stream *events.ChannelStream +} + +func (m *MockProgram) Name() string { return m.name } +func (m *MockProgram) Load(ctx context.Context) error { m.loaded = true; return nil } +func (m *MockProgram) Attach(ctx context.Context) error { m.attached = true; return nil } +// ... implement other methods + +func TestWithMock(t *testing.T) { + mock := &MockProgram{ + name: "mock_program", + stream: events.NewChannelStream(10), + } + + // Test program behavior without eBPF + ctx := context.Background() + if err := mock.Load(ctx); err != nil { + t.Fatalf("Mock load failed: %v", err) + } + + if !mock.loaded { + t.Error("Mock should be loaded") + } +} +``` + +### 4. Event Testing + +Test event creation and serialization: + +```go +func TestEventCreation(t *testing.T) { + metadata := map[string]interface{}{ + "test_field": "test_value", + "test_number": 42, + } + + event := events.NewBaseEvent( + "your_program", + 1234, + "test_cmd", + uint64(time.Now().UnixNano()), + metadata, + ) + + // Test event properties + if event.Type() != "your_program" { + t.Errorf("Expected type 'your_program', got %s", event.Type()) + } + + if event.PID() != 1234 { + t.Errorf("Expected PID 1234, got %d", event.PID()) + } + + // Test metadata + eventMetadata := event.Metadata() + if eventMetadata["test_field"] != "test_value" { + t.Errorf("Expected test_field 'test_value', got %v", eventMetadata["test_field"]) + } + + // Test JSON serialization + jsonData, err := event.MarshalJSON() + if err != nil { + t.Fatalf("JSON marshaling failed: %v", err) + } + + var unmarshaled map[string]interface{} + if err := json.Unmarshal(jsonData, &unmarshaled); err != nil { + t.Fatalf("JSON unmarshaling failed: %v", err) + } + + if unmarshaled["type"] != "your_program" { + t.Errorf("Expected JSON type 'your_program', got %v", unmarshaled["type"]) + } +} +``` + +### 5. Running Tests + +```bash +# Run unit tests +go test ./internal/programs/your_program + +# Run with verbose output +go test -v ./internal/programs/your_program + +# Run with race detection +go test -race ./internal/programs/your_program + +# Run all tests +go test ./... + +# Run integration tests (longer) +go test -v ./internal/programs/your_program -tags=integration +``` + } + + // Verify JSON contains expected fields + jsonStr := string(data) + if !strings.Contains(jsonStr, "\"custom_field1\":42") { + t.Errorf("JSON should contain custom_field1") + } +} + +func TestYourProgramCreation(t *testing.T) { + storage := bpf.NewMemoryStorage() + program := NewProgram(storage) + + if program.GetName() != "your_program" { + t.Errorf("Expected name 'your_program', got '%s'", program.GetName()) + } + + if program.IsRunning() { + t.Error("Program should not be running initially") + } +} +``` + +## Best Practices + +### eBPF Code + +1. **Keep it simple**: eBPF has limitations, avoid complex logic in kernel space +2. **Check bounds**: Always validate array/buffer access to prevent verifier rejection +3. **Handle errors**: Check return values from BPF helpers +4. **Minimize stack usage**: eBPF stack is limited to 512 bytes +5. **Use ring buffers**: Prefer ring buffers over perf buffers for event delivery +6. **Struct alignment**: Ensure consistent memory layout between C and Go + +Example of safe eBPF code: + +```c +SEC("tracepoint/syscalls/sys_enter_your_syscall") +int trace_your_syscall(struct trace_event_raw_sys_enter* ctx) { + struct your_event *event; + + // Always check ring buffer allocation + event = bpf_ringbuf_reserve(&events, sizeof(*event), 0); + if (!event) { + return 0; // Failed to allocate, but don't crash + } + + // Safe field access + event->pid = bpf_get_current_pid_tgid() >> 32; + event->timestamp = bpf_ktime_get_ns(); + + // Safe string operations + bpf_get_current_comm(&event->comm, sizeof(event->comm)); + + // Always submit or discard + bpf_ringbuf_submit(event, 0); + return 0; +} +``` + +### Go Code + +1. **Error handling**: Always handle errors, especially from eBPF operations +2. **Context awareness**: Respect cancellation contexts for graceful shutdown +3. **Resource cleanup**: Properly clean up eBPF resources +4. **Thread safety**: Use appropriate synchronization for concurrent access +5. **Structured logging**: Use the logger package for consistent debugging +6. **Interface compliance**: Implement all required interface methods + +Example of robust Go implementation: + +```go +func (p *Program) Attach(ctx context.Context) error { + if !p.IsLoaded() { + return fmt.Errorf("program not loaded") + } + + logger.Debugf("Attaching %s program", p.Name()) + + // Use context for cancellation + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + + // Attach with proper error handling + if err := p.AttachTracepoint(TracepointGroup, TracepointName, TracepointProgram); err != nil { + return fmt.Errorf("failed to attach tracepoint: %w", err) + } + + // Start processing with context + if err := p.StartEventProcessing(ctx, EventsMapName, p.parseEvent); err != nil { + // Clean up on failure + p.Detach(ctx) + return fmt.Errorf("failed to start event processing: %w", err) + } + + p.SetAttached(true) + logger.Debugf("✅ %s program attached successfully", p.Name()) + return nil +} +``` + +### Performance + +1. **Ring buffer sizing**: Choose appropriate buffer sizes (256KB is often good) +2. **Event filtering**: Filter events in eBPF when possible to reduce overhead +3. **Batch processing**: Process multiple events efficiently +4. **Memory management**: Avoid excessive allocations in hot paths +5. **Efficient parsing**: Parse only necessary fields from binary data + +### Error Handling + +1. **Graceful degradation**: Continue operation if non-critical features fail +2. **Meaningful errors**: Provide context in error messages +3. **Proper cleanup**: Always clean up resources on errors +4. **Logging**: Log errors with appropriate levels + +## Examples + +### Simple Tracepoint Program + +See `internal/programs/packet_drop/` for a complete tracepoint-based program that: +- Monitors kernel packet drop events +- Extracts drop reason and packet information +- Provides structured events with metadata + +### Syscall Monitoring Program + +See `internal/programs/connection/` for a syscall-based program that: +- Monitors `connect()` system calls +- Extracts network connection details +- Handles IPv4/IPv6 addresses and port information + +### Custom Program Structure + +Here's a minimal template for a new program: + +```bash +# 1. Create program directory +mkdir -p internal/programs/my_monitor + +# 2. Create main program file +cat > internal/programs/my_monitor/my_monitor.go << 'EOF' +package my_monitor + +import ( + "context" + "encoding/binary" + "fmt" + + "github.com/srodi/ebpf-server/internal/core" + "github.com/srodi/ebpf-server/internal/events" + "github.com/srodi/ebpf-server/internal/programs" + "github.com/srodi/ebpf-server/pkg/logger" +) + +const ( + ProgramName = "my_monitor" + ProgramDescription = "Monitors custom kernel events" + ObjectPath = "bpf/my_monitor.o" + TracepointProgram = "trace_my_event" + EventsMapName = "events" + TracepointGroup = "custom" + TracepointName = "my_event" +) + +type Program struct { + *programs.BaseProgram +} + +func NewProgram() *Program { + base := programs.NewBaseProgram(ProgramName, ProgramDescription, ObjectPath) + return &Program{BaseProgram: base} +} + +func (p *Program) Attach(ctx context.Context) error { + if !p.IsLoaded() { + return fmt.Errorf("program not loaded") + } + + logger.Debugf("Attaching %s program", ProgramName) + + if err := p.AttachTracepoint(TracepointGroup, TracepointName, TracepointProgram); err != nil { + return fmt.Errorf("failed to attach: %w", err) + } + + if err := p.StartEventProcessing(ctx, EventsMapName, p.parseEvent); err != nil { + return fmt.Errorf("failed to start processing: %w", err) + } + + p.SetAttached(true) + return nil +} + +func (p *Program) parseEvent(data []byte) (core.Event, error) { + if len(data) < 24 { + return nil, fmt.Errorf("insufficient data: %d bytes", len(data)) + } + + pid := binary.LittleEndian.Uint32(data[0:4]) + timestamp := binary.LittleEndian.Uint64(data[4:12]) + command := extractNullTerminatedString(data[12:]) + + metadata := map[string]interface{}{ + "custom_field": "custom_value", + } + + return events.NewBaseEvent(ProgramName, pid, command, timestamp, metadata), nil +} + +func extractNullTerminatedString(data []byte) string { + for i, b := range data { + if b == 0 { + return string(data[:i]) + } + } + return string(data) +} +EOF + +# 3. Create corresponding eBPF C code +cat > bpf/my_monitor.c << 'EOF' +#include "vmlinux.h" +#include "bpf_helpers.h" +#include "bpf_tracing.h" + +struct my_event { + u32 pid; + u64 timestamp; + char comm[16]; +}; + +struct { + __uint(type, BPF_MAP_TYPE_RINGBUF); + __uint(max_entries, 256 * 1024); +} events SEC(".maps"); + +SEC("tracepoint/custom/my_event") +int trace_my_event(void *ctx) { + struct my_event *event; + + event = bpf_ringbuf_reserve(&events, sizeof(*event), 0); + if (!event) { + return 0; + } + + event->pid = bpf_get_current_pid_tgid() >> 32; + event->timestamp = bpf_ktime_get_ns(); + bpf_get_current_comm(&event->comm, sizeof(event->comm)); + + bpf_ringbuf_submit(event, 0); + return 0; +} + +char LICENSE[] SEC("license") = "GPL"; +EOF + +# 4. Register in system +# Add to internal/system/system.go Initialize() method: +# myProgram := my_monitor.NewProgram() +# if err := s.manager.RegisterProgram(myProgram); err != nil { +# return fmt.Errorf("failed to register my_monitor: %w", err) +# } +``` + +## Troubleshooting + +### Common Issues + +1. **Program won't load**: + - Check eBPF verifier errors: `dmesg | grep bpf` + - Verify struct sizes and alignment + - Ensure all code paths are verified by the kernel + +2. **No events received**: + - Verify tracepoint/kprobe exists: `ls /sys/kernel/debug/tracing/events/` + - Check if events are being generated: `cat /sys/kernel/debug/tracing/trace_pipe` + - Validate ring buffer setup and reading + +3. **Permission errors**: + - Run with appropriate privileges (usually root) + - Check if BPF is enabled: `cat /proc/sys/kernel/unprivileged_bpf_disabled` + - Verify cgroup permissions for containers + +4. **Memory access violations**: + - Check struct alignment between C and Go + - Validate all memory accesses in eBPF code + - Use `bpf_core_read()` for safe kernel memory access + +### Debugging Tools + +1. **eBPF Tools**: + ```bash + # List loaded programs + sudo bpftool prog list + + # Show program details + sudo bpftool prog show id + + # List maps + sudo bpftool map list + + # Dump map contents + sudo bpftool map dump id + ``` + +2. **Kernel Tracing**: + ```bash + # View trace output + sudo cat /sys/kernel/debug/tracing/trace_pipe + + # Check available tracepoints + sudo ls /sys/kernel/debug/tracing/events/ + + # Enable specific tracepoint + echo 1 | sudo tee /sys/kernel/debug/tracing/events/your_subsystem/your_event/enable + ``` + +3. **Application Debugging**: + ```bash + # Run with debug logging + EBPF_DEBUG=1 ./ebpf-server + + # Race detection during testing + go test -race ./internal/programs/your_program + + # Memory profiling + go test -memprofile=mem.prof ./internal/programs/your_program + ``` + +### Development Tips + +1. **Start incrementally**: Begin with simple event capture, add complexity gradually +2. **Use existing programs**: Study `connection` and `packet_drop` programs as templates +3. **Test eBPF separately**: Use `bpf_printk()` and trace_pipe for initial debugging +4. **Validate structs**: Ensure C and Go struct layouts match exactly +5. **Handle edge cases**: Test with various input conditions and error scenarios +6. **Document your program**: Add clear comments and documentation +7. **Version compatibility**: Test with different kernel versions if targeting multiple systems + +## Summary + +This guide covered the complete process of developing eBPF monitoring programs for the eBPF Network Monitor: + +1. **Architecture**: Understanding the modular, interface-based design +2. **Implementation**: Creating programs using BaseProgram and core interfaces +3. **Events**: Using the unified event system with BaseEvent +4. **Integration**: Registering with the Manager and System +5. **API**: Automatic HTTP endpoints and custom endpoint creation +6. **Testing**: Comprehensive testing strategies from unit to integration tests +7. **Best Practices**: Performance, security, and maintainability guidelines + +The current architecture provides a solid foundation for adding new monitoring capabilities while maintaining consistency and reliability across all programs. + +For additional examples and implementation details, examine the existing programs in `internal/programs/connection/` and `internal/programs/packet_drop/`. diff --git a/docs/setup.md b/docs/setup.md new file mode 100644 index 0000000..fa11927 --- /dev/null +++ b/docs/setup.md @@ -0,0 +1,485 @@ +# Setup Guide + +This guide covers the setup and installation of the eBPF Network Monitor server. + +## Overview + +The eBPF Network Monitor is a modular system that uses eBPF programs to monitor network connections and packet drops in real-time. It features: + +- **Unified Event API**: Single `/api/events` endpoint for all monitoring data +- **Modular Architecture**: Clean separation between core interfaces, event system, and programs +- **Real-time Monitoring**: Live event streaming from kernel space +- **Interactive Documentation**: Built-in Swagger UI for API exploration + +## Table of Contents + +- [System Requirements](#system-requirements) +- [Dependencies](#dependencies) +- [Installation](#installation) +- [Configuration](#configuration) +- [Running the Server](#running-the-server) +- [Verification](#verification) +- [Troubleshooting](#troubleshooting) + +## System Requirements + +### Operating System +- **Linux kernel 4.18+** with eBPF support +- Supported distributions: + - Ubuntu 18.04+ + - Debian 10+ + - CentOS 8+ + - RHEL 8+ + - Fedora 30+ + +### Hardware +- **x86_64** architecture (required for current eBPF programs) +- **Minimum 2GB RAM** (4GB+ recommended for production) +- **Root/sudo privileges** (required for eBPF program loading) + +### Kernel Features +Verify your kernel supports the required eBPF features: + +```bash +# Check kernel version +uname -r + +# Verify eBPF support +zgrep CONFIG_BPF_SYSCALL /proc/config.gz +zgrep CONFIG_BPF_JIT /proc/config.gz + +# Check if BTF is enabled (recommended) +zgrep CONFIG_DEBUG_INFO_BTF /proc/config.gz +``` + +## Dependencies + +### Build Dependencies + +#### Ubuntu/Debian +```bash +sudo apt update +sudo apt install -y \ + golang-go \ + clang \ + llvm \ + libbpf-dev \ + linux-headers-$(uname -r) \ + build-essential \ + git \ + make +``` + +#### CentOS/RHEL/Fedora +```bash +# CentOS 8/RHEL 8 +sudo dnf install -y \ + golang \ + clang \ + llvm \ + libbpf-devel \ + kernel-headers \ + kernel-devel \ + make \ + git + +# Fedora +sudo dnf install -y \ + golang \ + clang \ + llvm \ + libbpf-devel \ + kernel-headers \ + kernel-devel \ + make \ + git +``` + +### Go Version +- **Go 1.23+** is required +- Verify installation: `go version` + +### Optional Dependencies + +#### For development and testing +```bash +# Ubuntu/Debian +sudo apt install -y \ + bpftool \ + linux-tools-common \ + linux-tools-$(uname -r) + +# CentOS/RHEL/Fedora +sudo dnf install -y bpftool +``` + +## Installation + +### 1. Clone Repository +```bash +git clone https://github.com/srodi/ebpf-server.git +cd ebpf-server +``` + +### 2. Build eBPF Programs +```bash +# Compile eBPF bytecode +make build-bpf +``` + +### 3. Build Go Server +```bash +# Build production binary +make build + +# Or build development binary with debug logging +make build-dev +``` + +### 4. Verify Build +```bash +ls -la bin/ +# Should show ebpf-server binary +``` + +## Configuration + +### Command-Line Options + +The server supports these command-line options: + +```bash +# Start server on custom address/port +./bin/ebpf-server -addr=":9090" + +# Default is :8080 +./bin/ebpf-server +``` + +### Environment Variables + +Configure logging and debugging: + +```bash +# Logging configuration (handled by logger package) +export EBPF_LOG_LEVEL=debug # debug, info, warn, error (default: info) + +# For development debugging +export EBPF_DEBUG=1 # Enable debug mode +``` + +### Configuration File (Future Enhancement) + +The current version uses command-line configuration. Future versions may support: + +```yaml +# config.yaml (not yet implemented) +server: + host: "0.0.0.0" + port: 8080 + +logging: + level: "info" + format: "json" +``` + +## Running the Server + +### Development Mode +```bash +# Run with debug logging +sudo ./bin/ebpf-server-dev + +# Run on custom port +sudo ./bin/ebpf-server-dev -addr=":9090" +``` + +### Production Mode +```bash +# Run production server (default port 8080) +sudo ./bin/ebpf-server + +# Run on custom address/port +sudo ./bin/ebpf-server -addr="0.0.0.0:8080" +``` + +### Background Service +```bash +# Run as background service +sudo nohup ./bin/ebpf-server > /var/log/ebpf-server.log 2>&1 & +``` + +### Docker (If Available) +```bash +# Build Docker image +docker build -t ebpf-server . + +# Run with required privileges +docker run --privileged \ + -p 8080:8080 \ + -v /sys/kernel/debug:/sys/kernel/debug:ro \ + -v /proc:/host/proc:ro \ + ebpf-server +``` + +### Systemd Service + +Create `/etc/systemd/system/ebpf-server.service`: + +```ini +[Unit] +Description=eBPF Network Monitor Server +After=network.target + +[Service] +Type=simple +User=root +ExecStart=/usr/local/bin/ebpf-server -addr=":8080" +Restart=always +RestartSec=5 +StandardOutput=journal +StandardError=journal +Environment=EBPF_LOG_LEVEL=info + +[Install] +WantedBy=multi-user.target +``` + +```bash +# Install and start service +sudo cp bin/ebpf-server /usr/local/bin/ +sudo systemctl daemon-reload +sudo systemctl enable ebpf-server +sudo systemctl start ebpf-server +``` + +## Verification + +### 1. Check Server Status +```bash +# Check if server is running +curl http://localhost:8080/health + +# Expected response: +# {"service":"ebpf-server","status":"healthy","version":"v1.0.0"} +``` + +### 2. Verify eBPF Programs +```bash +# List active programs +curl http://localhost:8080/api/programs + +# Check eBPF programs in kernel +sudo bpftool prog list | grep ebpf-server +``` + +### 3. Test API Endpoints +```bash +# Test unified events API +curl "http://localhost:8080/api/events?limit=10" + +# Test connection events specifically +curl "http://localhost:8080/api/events?type=connection&limit=5" + +# Test packet drop events specifically +curl "http://localhost:8080/api/events?type=packet_drop&limit=5" + +# Test events with time filter +curl "http://localhost:8080/api/events?since=2023-01-01T00:00:00Z&limit=10" + +# Test events for specific process +curl "http://localhost:8080/api/events?pid=1234&limit=10" +``` + +### 4. View Documentation +```bash +# Open Swagger documentation in browser +xdg-open http://localhost:8080/docs/ + +# View available endpoints +curl http://localhost:8080/ + +# Access Swagger JSON directly +curl http://localhost:8080/docs/swagger.json +``` + +### 5. Available API Endpoints + +The server provides these endpoints: + +**Core Unified APIs (Recommended):** +- `GET /api/events` - Query all events with filtering +- `GET /api/programs` - List eBPF program status +- `GET /health` - Health check + +**Legacy Specific APIs:** +- `POST /api/connection-summary` - Connection statistics +- `POST /api/packet-drop-summary` - Packet drop statistics +- `GET /api/list-connections` - List connection events +- `GET /api/list-packet-drops` - List packet drop events + +**Documentation:** +- `GET /docs/` - Interactive Swagger UI +- `GET /` - API overview and endpoint list + +## Troubleshooting + +### Common Issues + +#### Permission Denied +```bash +# Error: permission denied loading eBPF program +# Solution: Run with sudo/root privileges +sudo ./bin/ebpf-server +``` + +#### Missing Kernel Headers +```bash +# Error: cannot find kernel headers +# Solution: Install kernel headers for your kernel version +sudo apt install linux-headers-$(uname -r) # Ubuntu/Debian +sudo dnf install kernel-devel kernel-headers # CentOS/RHEL/Fedora +``` + +#### eBPF Verifier Errors +```bash +# Check kernel logs for eBPF verifier errors +sudo dmesg | grep bpf + +# Common solutions: +# 1. Update to newer kernel version +# 2. Check eBPF program complexity +# 3. Verify struct alignment +``` + +#### Port Already in Use +```bash +# Error: address already in use +# Solution: Change port using command-line flag or kill existing process +sudo ./bin/ebpf-server -addr=":8081" +# Or +sudo lsof -i :8080 +sudo kill +``` + +#### No Events Appearing +```bash +# Check if eBPF programs are loaded and attached +sudo bpftool prog list | grep -E "(connection|packet_drop)" +sudo bpftool link list + +# Check program status via API +curl http://localhost:8080/api/programs + +# Check specific tracepoints exist +sudo ls /sys/kernel/debug/tracing/events/syscalls/ | grep sys_enter_connect +sudo ls /sys/kernel/debug/tracing/events/skb/ | grep kfree_skb + +# Generate some test traffic +ping google.com & +curl http://google.com +killall ping + +# Check for events +curl "http://localhost:8080/api/events?limit=5" + +# Enable more verbose logging +export EBPF_DEBUG=1 +sudo -E ./bin/ebpf-server-dev +``` + +### Debugging Tools + +#### eBPF Debugging +```bash +# List loaded eBPF programs +sudo bpftool prog list + +# Show program details +sudo bpftool prog show id + +# List eBPF maps +sudo bpftool map list + +# View map contents +sudo bpftool map dump id +``` + +#### Network Tracing +```bash +# View kernel trace events +sudo cat /sys/kernel/debug/tracing/trace_pipe + +# Enable specific tracepoints +echo 1 | sudo tee /sys/kernel/debug/tracing/events/net/net_dev_queue/enable +``` + +#### System Monitoring +```bash +# Monitor system resources +htop +iostat 1 +netstat -tlnp | grep 8080 + +# Check system logs +journalctl -u ebpf-server -f +``` + +### Performance Tuning + +#### Event Buffer Management +```bash +# Monitor event processing performance +curl "http://localhost:8080/api/programs" | jq '.programs[] | {name, event_count}' + +# Check for dropped events in system logs +journalctl -u ebpf-server | grep -i "dropped\|full\|overflow" +``` + +#### System Resource Optimization +```bash +# Pin server to specific CPUs for consistent performance +taskset -c 0,1 ./bin/ebpf-server + +# Set memory limits for production +ulimit -m 1048576 # 1GB limit + +# Increase ring buffer size if experiencing drops (future enhancement) +# Currently ring buffer size is compiled into eBPF programs +``` + +#### API Performance +```bash +# Use specific filters to reduce data transfer +curl "http://localhost:8080/api/events?type=connection&limit=100" + +# Use time-based filtering for better performance +curl "http://localhost:8080/api/events?since=$(date -d '1 hour ago' -Iseconds)" +``` + +### Log Analysis + +#### Enable Debug Logging +```bash +export EBPF_LOG_LEVEL=debug +sudo ./bin/ebpf-server-dev 2>&1 | tee debug.log +``` + +#### Common Log Messages +- `"eBPF program loaded successfully"` - Program loaded correctly +- `"Failed to attach eBPF program"` - Attachment failed, check permissions +- `"Ring buffer event received"` - Events are being processed +- `"Event channel full"` - Consider increasing buffer sizes + +### Getting Help + +1. **Check logs**: Review server logs for error messages +2. **Verify system**: Ensure all dependencies are installed +3. **Test incrementally**: Start with basic functionality +4. **Check documentation**: Review API documentation at `/docs` +5. **Community support**: Report issues on GitHub + +For additional support, please check: +- [Program Development Guide](program-development.md) +- [GitHub Issues](https://github.com/srodi/ebpf-server/issues) +- [API Documentation](https://petstore.swagger.io/?url=https://raw.githubusercontent.com/srodi/ebpf-server/main/docs/swagger/swagger.json) diff --git a/docs/swagger/docs.go b/docs/swagger/docs.go new file mode 100644 index 0000000..d5e136a --- /dev/null +++ b/docs/swagger/docs.go @@ -0,0 +1,748 @@ +// Package swagger Code generated by swaggo/swag. DO NOT EDIT +package swagger + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "contact": { + "name": "API Support", + "url": "https://github.com/srodi/ebpf-server/issues", + "email": "support@example.com" + }, + "license": { + "name": "MIT", + "url": "https://github.com/srodi/ebpf-server/blob/main/LICENSE" + }, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/api/connection-summary": { + "get": { + "description": "Get count of connection events filtered by PID, command, and time window", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "connections" + ], + "summary": "Get connection statistics", + "parameters": [ + { + "type": "integer", + "description": "Process ID (GET only)", + "name": "pid", + "in": "query" + }, + { + "type": "string", + "description": "Command name (GET only)", + "name": "command", + "in": "query" + }, + { + "type": "integer", + "description": "Duration in seconds (GET only, default: 60)", + "name": "duration_seconds", + "in": "query" + }, + { + "description": "Connection summary request (POST only)", + "name": "request", + "in": "body", + "schema": { + "$ref": "#/definitions/internal_api.ConnectionSummaryRequest" + } + } + ], + "responses": { + "200": { + "description": "Connection statistics", + "schema": { + "$ref": "#/definitions/internal_api.ConnectionSummaryResponse" + } + }, + "400": { + "description": "Bad request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal server error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "503": { + "description": "Service unavailable", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "post": { + "description": "Get count of connection events filtered by PID, command, and time window", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "connections" + ], + "summary": "Get connection statistics", + "parameters": [ + { + "type": "integer", + "description": "Process ID (GET only)", + "name": "pid", + "in": "query" + }, + { + "type": "string", + "description": "Command name (GET only)", + "name": "command", + "in": "query" + }, + { + "type": "integer", + "description": "Duration in seconds (GET only, default: 60)", + "name": "duration_seconds", + "in": "query" + }, + { + "description": "Connection summary request (POST only)", + "name": "request", + "in": "body", + "schema": { + "$ref": "#/definitions/internal_api.ConnectionSummaryRequest" + } + } + ], + "responses": { + "200": { + "description": "Connection statistics", + "schema": { + "$ref": "#/definitions/internal_api.ConnectionSummaryResponse" + } + }, + "400": { + "description": "Bad request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal server error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "503": { + "description": "Service unavailable", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/events": { + "get": { + "description": "Get events filtered by type, PID, command, time range, and limit", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "events" + ], + "summary": "Query events", + "parameters": [ + { + "type": "string", + "description": "Event type (connection, packet_drop)", + "name": "type", + "in": "query" + }, + { + "type": "integer", + "description": "Process ID", + "name": "pid", + "in": "query" + }, + { + "type": "string", + "description": "Command name", + "name": "command", + "in": "query" + }, + { + "type": "string", + "description": "Start time (RFC3339 format)", + "name": "since", + "in": "query" + }, + { + "type": "string", + "description": "End time (RFC3339 format)", + "name": "until", + "in": "query" + }, + { + "type": "integer", + "description": "Maximum number of events to return (default: 100)", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Filtered events", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "Internal server error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "503": { + "description": "Service unavailable", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/list-connections": { + "get": { + "description": "Get recent connection events grouped by PID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "connections" + ], + "summary": "List connection events", + "responses": { + "200": { + "description": "Connection events", + "schema": { + "$ref": "#/definitions/internal_api.ConnectionListResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "503": { + "description": "Service unavailable", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/list-packet-drops": { + "get": { + "description": "Get recent packet drop events grouped by PID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "packet_drops" + ], + "summary": "List packet drop events", + "responses": { + "200": { + "description": "Packet drop events", + "schema": { + "$ref": "#/definitions/internal_api.PacketDropListResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "503": { + "description": "Service unavailable", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/packet-drop-summary": { + "get": { + "description": "Get count of packet drop events filtered by PID, command, and time window", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "packet_drops" + ], + "summary": "Get packet drop statistics", + "parameters": [ + { + "type": "integer", + "description": "Process ID (GET only)", + "name": "pid", + "in": "query" + }, + { + "type": "string", + "description": "Command name (GET only)", + "name": "command", + "in": "query" + }, + { + "type": "integer", + "description": "Duration in seconds (GET only, default: 60)", + "name": "duration_seconds", + "in": "query" + }, + { + "description": "Packet drop summary request (POST only)", + "name": "request", + "in": "body", + "schema": { + "$ref": "#/definitions/internal_api.PacketDropSummaryRequest" + } + } + ], + "responses": { + "200": { + "description": "Packet drop statistics", + "schema": { + "$ref": "#/definitions/internal_api.PacketDropSummaryResponse" + } + }, + "400": { + "description": "Bad request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal server error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "503": { + "description": "Service unavailable", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "post": { + "description": "Get count of packet drop events filtered by PID, command, and time window", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "packet_drops" + ], + "summary": "Get packet drop statistics", + "parameters": [ + { + "type": "integer", + "description": "Process ID (GET only)", + "name": "pid", + "in": "query" + }, + { + "type": "string", + "description": "Command name (GET only)", + "name": "command", + "in": "query" + }, + { + "type": "integer", + "description": "Duration in seconds (GET only, default: 60)", + "name": "duration_seconds", + "in": "query" + }, + { + "description": "Packet drop summary request (POST only)", + "name": "request", + "in": "body", + "schema": { + "$ref": "#/definitions/internal_api.PacketDropSummaryRequest" + } + } + ], + "responses": { + "200": { + "description": "Packet drop statistics", + "schema": { + "$ref": "#/definitions/internal_api.PacketDropSummaryResponse" + } + }, + "400": { + "description": "Bad request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal server error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "503": { + "description": "Service unavailable", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/programs": { + "get": { + "description": "Get the status and information of all loaded eBPF programs", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "programs" + ], + "summary": "List eBPF programs", + "responses": { + "200": { + "description": "List of eBPF programs", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "Internal server error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "503": { + "description": "Service unavailable", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/health": { + "get": { + "description": "Get the health status of the eBPF monitoring system", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "health" + ], + "summary": "Health check", + "responses": { + "200": { + "description": "Health status", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "503": { + "description": "Service unavailable", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + } + }, + "definitions": { + "internal_api.ConnectionListResponse": { + "type": "object", + "properties": { + "events_by_pid": { + "description": "Events grouped by PID", + "type": "object", + "additionalProperties": { + "type": "array", + "items": {} + } + }, + "query_time": { + "description": "Query timestamp", + "type": "string", + "example": "2023-01-01T12:00:00Z" + }, + "total_events": { + "description": "Total number of events", + "type": "integer", + "example": 10 + }, + "total_pids": { + "description": "Number of unique PIDs", + "type": "integer", + "example": 3 + } + } + }, + "internal_api.ConnectionSummaryRequest": { + "type": "object", + "properties": { + "command": { + "description": "Command name", + "type": "string", + "example": "curl" + }, + "duration_seconds": { + "description": "Duration in seconds", + "type": "integer", + "example": 60 + }, + "pid": { + "description": "Process ID", + "type": "integer", + "example": 1234 + } + } + }, + "internal_api.ConnectionSummaryResponse": { + "type": "object", + "properties": { + "command": { + "description": "Command name", + "type": "string", + "example": "curl" + }, + "count": { + "description": "Number of connection events", + "type": "integer", + "example": 5 + }, + "duration_seconds": { + "description": "Duration in seconds", + "type": "integer", + "example": 60 + }, + "pid": { + "description": "Process ID", + "type": "integer", + "example": 1234 + }, + "query_time": { + "description": "Query timestamp", + "type": "string", + "example": "2023-01-01T12:00:00Z" + } + } + }, + "internal_api.PacketDropListResponse": { + "type": "object", + "properties": { + "events_by_pid": { + "description": "Events grouped by PID", + "type": "object", + "additionalProperties": { + "type": "array", + "items": {} + } + }, + "query_time": { + "description": "Query timestamp", + "type": "string", + "example": "2023-01-01T12:00:00Z" + }, + "total_events": { + "description": "Total number of events", + "type": "integer", + "example": 7 + }, + "total_pids": { + "description": "Number of unique PIDs", + "type": "integer", + "example": 2 + } + } + }, + "internal_api.PacketDropSummaryRequest": { + "type": "object", + "properties": { + "command": { + "description": "Command name", + "type": "string", + "example": "nginx" + }, + "duration_seconds": { + "description": "Duration in seconds", + "type": "integer", + "example": 60 + }, + "pid": { + "description": "Process ID", + "type": "integer", + "example": 1234 + } + } + }, + "internal_api.PacketDropSummaryResponse": { + "type": "object", + "properties": { + "command": { + "description": "Command name", + "type": "string", + "example": "nginx" + }, + "count": { + "description": "Number of packet drop events", + "type": "integer", + "example": 3 + }, + "duration_seconds": { + "description": "Duration in seconds", + "type": "integer", + "example": 60 + }, + "pid": { + "description": "Process ID", + "type": "integer", + "example": 1234 + }, + "query_time": { + "description": "Query timestamp", + "type": "string", + "example": "2023-01-01T12:00:00Z" + } + } + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "1.0.0", + Host: "localhost:8080", + BasePath: "/", + Schemes: []string{}, + Title: "eBPF Network Monitor API", + Description: "HTTP API for eBPF-based network connection and packet drop monitoring", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/docs/swagger/swagger.json b/docs/swagger/swagger.json new file mode 100644 index 0000000..8365979 --- /dev/null +++ b/docs/swagger/swagger.json @@ -0,0 +1,724 @@ +{ + "swagger": "2.0", + "info": { + "description": "HTTP API for eBPF-based network connection and packet drop monitoring", + "title": "eBPF Network Monitor API", + "contact": { + "name": "API Support", + "url": "https://github.com/srodi/ebpf-server/issues", + "email": "support@example.com" + }, + "license": { + "name": "MIT", + "url": "https://github.com/srodi/ebpf-server/blob/main/LICENSE" + }, + "version": "1.0.0" + }, + "host": "localhost:8080", + "basePath": "/", + "paths": { + "/api/connection-summary": { + "get": { + "description": "Get count of connection events filtered by PID, command, and time window", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "connections" + ], + "summary": "Get connection statistics", + "parameters": [ + { + "type": "integer", + "description": "Process ID (GET only)", + "name": "pid", + "in": "query" + }, + { + "type": "string", + "description": "Command name (GET only)", + "name": "command", + "in": "query" + }, + { + "type": "integer", + "description": "Duration in seconds (GET only, default: 60)", + "name": "duration_seconds", + "in": "query" + }, + { + "description": "Connection summary request (POST only)", + "name": "request", + "in": "body", + "schema": { + "$ref": "#/definitions/internal_api.ConnectionSummaryRequest" + } + } + ], + "responses": { + "200": { + "description": "Connection statistics", + "schema": { + "$ref": "#/definitions/internal_api.ConnectionSummaryResponse" + } + }, + "400": { + "description": "Bad request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal server error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "503": { + "description": "Service unavailable", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "post": { + "description": "Get count of connection events filtered by PID, command, and time window", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "connections" + ], + "summary": "Get connection statistics", + "parameters": [ + { + "type": "integer", + "description": "Process ID (GET only)", + "name": "pid", + "in": "query" + }, + { + "type": "string", + "description": "Command name (GET only)", + "name": "command", + "in": "query" + }, + { + "type": "integer", + "description": "Duration in seconds (GET only, default: 60)", + "name": "duration_seconds", + "in": "query" + }, + { + "description": "Connection summary request (POST only)", + "name": "request", + "in": "body", + "schema": { + "$ref": "#/definitions/internal_api.ConnectionSummaryRequest" + } + } + ], + "responses": { + "200": { + "description": "Connection statistics", + "schema": { + "$ref": "#/definitions/internal_api.ConnectionSummaryResponse" + } + }, + "400": { + "description": "Bad request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal server error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "503": { + "description": "Service unavailable", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/events": { + "get": { + "description": "Get events filtered by type, PID, command, time range, and limit", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "events" + ], + "summary": "Query events", + "parameters": [ + { + "type": "string", + "description": "Event type (connection, packet_drop)", + "name": "type", + "in": "query" + }, + { + "type": "integer", + "description": "Process ID", + "name": "pid", + "in": "query" + }, + { + "type": "string", + "description": "Command name", + "name": "command", + "in": "query" + }, + { + "type": "string", + "description": "Start time (RFC3339 format)", + "name": "since", + "in": "query" + }, + { + "type": "string", + "description": "End time (RFC3339 format)", + "name": "until", + "in": "query" + }, + { + "type": "integer", + "description": "Maximum number of events to return (default: 100)", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Filtered events", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "Internal server error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "503": { + "description": "Service unavailable", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/list-connections": { + "get": { + "description": "Get recent connection events grouped by PID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "connections" + ], + "summary": "List connection events", + "responses": { + "200": { + "description": "Connection events", + "schema": { + "$ref": "#/definitions/internal_api.ConnectionListResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "503": { + "description": "Service unavailable", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/list-packet-drops": { + "get": { + "description": "Get recent packet drop events grouped by PID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "packet_drops" + ], + "summary": "List packet drop events", + "responses": { + "200": { + "description": "Packet drop events", + "schema": { + "$ref": "#/definitions/internal_api.PacketDropListResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "503": { + "description": "Service unavailable", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/packet-drop-summary": { + "get": { + "description": "Get count of packet drop events filtered by PID, command, and time window", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "packet_drops" + ], + "summary": "Get packet drop statistics", + "parameters": [ + { + "type": "integer", + "description": "Process ID (GET only)", + "name": "pid", + "in": "query" + }, + { + "type": "string", + "description": "Command name (GET only)", + "name": "command", + "in": "query" + }, + { + "type": "integer", + "description": "Duration in seconds (GET only, default: 60)", + "name": "duration_seconds", + "in": "query" + }, + { + "description": "Packet drop summary request (POST only)", + "name": "request", + "in": "body", + "schema": { + "$ref": "#/definitions/internal_api.PacketDropSummaryRequest" + } + } + ], + "responses": { + "200": { + "description": "Packet drop statistics", + "schema": { + "$ref": "#/definitions/internal_api.PacketDropSummaryResponse" + } + }, + "400": { + "description": "Bad request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal server error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "503": { + "description": "Service unavailable", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "post": { + "description": "Get count of packet drop events filtered by PID, command, and time window", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "packet_drops" + ], + "summary": "Get packet drop statistics", + "parameters": [ + { + "type": "integer", + "description": "Process ID (GET only)", + "name": "pid", + "in": "query" + }, + { + "type": "string", + "description": "Command name (GET only)", + "name": "command", + "in": "query" + }, + { + "type": "integer", + "description": "Duration in seconds (GET only, default: 60)", + "name": "duration_seconds", + "in": "query" + }, + { + "description": "Packet drop summary request (POST only)", + "name": "request", + "in": "body", + "schema": { + "$ref": "#/definitions/internal_api.PacketDropSummaryRequest" + } + } + ], + "responses": { + "200": { + "description": "Packet drop statistics", + "schema": { + "$ref": "#/definitions/internal_api.PacketDropSummaryResponse" + } + }, + "400": { + "description": "Bad request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal server error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "503": { + "description": "Service unavailable", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/programs": { + "get": { + "description": "Get the status and information of all loaded eBPF programs", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "programs" + ], + "summary": "List eBPF programs", + "responses": { + "200": { + "description": "List of eBPF programs", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "Internal server error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "503": { + "description": "Service unavailable", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/health": { + "get": { + "description": "Get the health status of the eBPF monitoring system", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "health" + ], + "summary": "Health check", + "responses": { + "200": { + "description": "Health status", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "503": { + "description": "Service unavailable", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + } + }, + "definitions": { + "internal_api.ConnectionListResponse": { + "type": "object", + "properties": { + "events_by_pid": { + "description": "Events grouped by PID", + "type": "object", + "additionalProperties": { + "type": "array", + "items": {} + } + }, + "query_time": { + "description": "Query timestamp", + "type": "string", + "example": "2023-01-01T12:00:00Z" + }, + "total_events": { + "description": "Total number of events", + "type": "integer", + "example": 10 + }, + "total_pids": { + "description": "Number of unique PIDs", + "type": "integer", + "example": 3 + } + } + }, + "internal_api.ConnectionSummaryRequest": { + "type": "object", + "properties": { + "command": { + "description": "Command name", + "type": "string", + "example": "curl" + }, + "duration_seconds": { + "description": "Duration in seconds", + "type": "integer", + "example": 60 + }, + "pid": { + "description": "Process ID", + "type": "integer", + "example": 1234 + } + } + }, + "internal_api.ConnectionSummaryResponse": { + "type": "object", + "properties": { + "command": { + "description": "Command name", + "type": "string", + "example": "curl" + }, + "count": { + "description": "Number of connection events", + "type": "integer", + "example": 5 + }, + "duration_seconds": { + "description": "Duration in seconds", + "type": "integer", + "example": 60 + }, + "pid": { + "description": "Process ID", + "type": "integer", + "example": 1234 + }, + "query_time": { + "description": "Query timestamp", + "type": "string", + "example": "2023-01-01T12:00:00Z" + } + } + }, + "internal_api.PacketDropListResponse": { + "type": "object", + "properties": { + "events_by_pid": { + "description": "Events grouped by PID", + "type": "object", + "additionalProperties": { + "type": "array", + "items": {} + } + }, + "query_time": { + "description": "Query timestamp", + "type": "string", + "example": "2023-01-01T12:00:00Z" + }, + "total_events": { + "description": "Total number of events", + "type": "integer", + "example": 7 + }, + "total_pids": { + "description": "Number of unique PIDs", + "type": "integer", + "example": 2 + } + } + }, + "internal_api.PacketDropSummaryRequest": { + "type": "object", + "properties": { + "command": { + "description": "Command name", + "type": "string", + "example": "nginx" + }, + "duration_seconds": { + "description": "Duration in seconds", + "type": "integer", + "example": 60 + }, + "pid": { + "description": "Process ID", + "type": "integer", + "example": 1234 + } + } + }, + "internal_api.PacketDropSummaryResponse": { + "type": "object", + "properties": { + "command": { + "description": "Command name", + "type": "string", + "example": "nginx" + }, + "count": { + "description": "Number of packet drop events", + "type": "integer", + "example": 3 + }, + "duration_seconds": { + "description": "Duration in seconds", + "type": "integer", + "example": 60 + }, + "pid": { + "description": "Process ID", + "type": "integer", + "example": 1234 + }, + "query_time": { + "description": "Query timestamp", + "type": "string", + "example": "2023-01-01T12:00:00Z" + } + } + } + } +} \ No newline at end of file diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml new file mode 100644 index 0000000..2e84124 --- /dev/null +++ b/docs/swagger/swagger.yaml @@ -0,0 +1,497 @@ +basePath: / +definitions: + internal_api.ConnectionListResponse: + properties: + events_by_pid: + additionalProperties: + items: {} + type: array + description: Events grouped by PID + type: object + query_time: + description: Query timestamp + example: "2023-01-01T12:00:00Z" + type: string + total_events: + description: Total number of events + example: 10 + type: integer + total_pids: + description: Number of unique PIDs + example: 3 + type: integer + type: object + internal_api.ConnectionSummaryRequest: + properties: + command: + description: Command name + example: curl + type: string + duration_seconds: + description: Duration in seconds + example: 60 + type: integer + pid: + description: Process ID + example: 1234 + type: integer + type: object + internal_api.ConnectionSummaryResponse: + properties: + command: + description: Command name + example: curl + type: string + count: + description: Number of connection events + example: 5 + type: integer + duration_seconds: + description: Duration in seconds + example: 60 + type: integer + pid: + description: Process ID + example: 1234 + type: integer + query_time: + description: Query timestamp + example: "2023-01-01T12:00:00Z" + type: string + type: object + internal_api.PacketDropListResponse: + properties: + events_by_pid: + additionalProperties: + items: {} + type: array + description: Events grouped by PID + type: object + query_time: + description: Query timestamp + example: "2023-01-01T12:00:00Z" + type: string + total_events: + description: Total number of events + example: 7 + type: integer + total_pids: + description: Number of unique PIDs + example: 2 + type: integer + type: object + internal_api.PacketDropSummaryRequest: + properties: + command: + description: Command name + example: nginx + type: string + duration_seconds: + description: Duration in seconds + example: 60 + type: integer + pid: + description: Process ID + example: 1234 + type: integer + type: object + internal_api.PacketDropSummaryResponse: + properties: + command: + description: Command name + example: nginx + type: string + count: + description: Number of packet drop events + example: 3 + type: integer + duration_seconds: + description: Duration in seconds + example: 60 + type: integer + pid: + description: Process ID + example: 1234 + type: integer + query_time: + description: Query timestamp + example: "2023-01-01T12:00:00Z" + type: string + type: object +host: localhost:8080 +info: + contact: + email: support@example.com + name: API Support + url: https://github.com/srodi/ebpf-server/issues + description: HTTP API for eBPF-based network connection and packet drop monitoring + license: + name: MIT + url: https://github.com/srodi/ebpf-server/blob/main/LICENSE + title: eBPF Network Monitor API + version: 1.0.0 +paths: + /api/connection-summary: + get: + consumes: + - application/json + description: Get count of connection events filtered by PID, command, and time + window + parameters: + - description: Process ID (GET only) + in: query + name: pid + type: integer + - description: Command name (GET only) + in: query + name: command + type: string + - description: 'Duration in seconds (GET only, default: 60)' + in: query + name: duration_seconds + type: integer + - description: Connection summary request (POST only) + in: body + name: request + schema: + $ref: '#/definitions/internal_api.ConnectionSummaryRequest' + produces: + - application/json + responses: + "200": + description: Connection statistics + schema: + $ref: '#/definitions/internal_api.ConnectionSummaryResponse' + "400": + description: Bad request + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal server error + schema: + additionalProperties: + type: string + type: object + "503": + description: Service unavailable + schema: + additionalProperties: + type: string + type: object + summary: Get connection statistics + tags: + - connections + post: + consumes: + - application/json + description: Get count of connection events filtered by PID, command, and time + window + parameters: + - description: Process ID (GET only) + in: query + name: pid + type: integer + - description: Command name (GET only) + in: query + name: command + type: string + - description: 'Duration in seconds (GET only, default: 60)' + in: query + name: duration_seconds + type: integer + - description: Connection summary request (POST only) + in: body + name: request + schema: + $ref: '#/definitions/internal_api.ConnectionSummaryRequest' + produces: + - application/json + responses: + "200": + description: Connection statistics + schema: + $ref: '#/definitions/internal_api.ConnectionSummaryResponse' + "400": + description: Bad request + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal server error + schema: + additionalProperties: + type: string + type: object + "503": + description: Service unavailable + schema: + additionalProperties: + type: string + type: object + summary: Get connection statistics + tags: + - connections + /api/events: + get: + consumes: + - application/json + description: Get events filtered by type, PID, command, time range, and limit + parameters: + - description: Event type (connection, packet_drop) + in: query + name: type + type: string + - description: Process ID + in: query + name: pid + type: integer + - description: Command name + in: query + name: command + type: string + - description: Start time (RFC3339 format) + in: query + name: since + type: string + - description: End time (RFC3339 format) + in: query + name: until + type: string + - description: 'Maximum number of events to return (default: 100)' + in: query + name: limit + type: integer + produces: + - application/json + responses: + "200": + description: Filtered events + schema: + additionalProperties: true + type: object + "500": + description: Internal server error + schema: + additionalProperties: + type: string + type: object + "503": + description: Service unavailable + schema: + additionalProperties: + type: string + type: object + summary: Query events + tags: + - events + /api/list-connections: + get: + consumes: + - application/json + description: Get recent connection events grouped by PID + produces: + - application/json + responses: + "200": + description: Connection events + schema: + $ref: '#/definitions/internal_api.ConnectionListResponse' + "500": + description: Internal server error + schema: + additionalProperties: + type: string + type: object + "503": + description: Service unavailable + schema: + additionalProperties: + type: string + type: object + summary: List connection events + tags: + - connections + /api/list-packet-drops: + get: + consumes: + - application/json + description: Get recent packet drop events grouped by PID + produces: + - application/json + responses: + "200": + description: Packet drop events + schema: + $ref: '#/definitions/internal_api.PacketDropListResponse' + "500": + description: Internal server error + schema: + additionalProperties: + type: string + type: object + "503": + description: Service unavailable + schema: + additionalProperties: + type: string + type: object + summary: List packet drop events + tags: + - packet_drops + /api/packet-drop-summary: + get: + consumes: + - application/json + description: Get count of packet drop events filtered by PID, command, and time + window + parameters: + - description: Process ID (GET only) + in: query + name: pid + type: integer + - description: Command name (GET only) + in: query + name: command + type: string + - description: 'Duration in seconds (GET only, default: 60)' + in: query + name: duration_seconds + type: integer + - description: Packet drop summary request (POST only) + in: body + name: request + schema: + $ref: '#/definitions/internal_api.PacketDropSummaryRequest' + produces: + - application/json + responses: + "200": + description: Packet drop statistics + schema: + $ref: '#/definitions/internal_api.PacketDropSummaryResponse' + "400": + description: Bad request + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal server error + schema: + additionalProperties: + type: string + type: object + "503": + description: Service unavailable + schema: + additionalProperties: + type: string + type: object + summary: Get packet drop statistics + tags: + - packet_drops + post: + consumes: + - application/json + description: Get count of packet drop events filtered by PID, command, and time + window + parameters: + - description: Process ID (GET only) + in: query + name: pid + type: integer + - description: Command name (GET only) + in: query + name: command + type: string + - description: 'Duration in seconds (GET only, default: 60)' + in: query + name: duration_seconds + type: integer + - description: Packet drop summary request (POST only) + in: body + name: request + schema: + $ref: '#/definitions/internal_api.PacketDropSummaryRequest' + produces: + - application/json + responses: + "200": + description: Packet drop statistics + schema: + $ref: '#/definitions/internal_api.PacketDropSummaryResponse' + "400": + description: Bad request + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal server error + schema: + additionalProperties: + type: string + type: object + "503": + description: Service unavailable + schema: + additionalProperties: + type: string + type: object + summary: Get packet drop statistics + tags: + - packet_drops + /api/programs: + get: + consumes: + - application/json + description: Get the status and information of all loaded eBPF programs + produces: + - application/json + responses: + "200": + description: List of eBPF programs + schema: + additionalProperties: true + type: object + "500": + description: Internal server error + schema: + additionalProperties: + type: string + type: object + "503": + description: Service unavailable + schema: + additionalProperties: + type: string + type: object + summary: List eBPF programs + tags: + - programs + /health: + get: + consumes: + - application/json + description: Get the health status of the eBPF monitoring system + produces: + - application/json + responses: + "200": + description: Health status + schema: + additionalProperties: true + type: object + "503": + description: Service unavailable + schema: + additionalProperties: + type: string + type: object + summary: Health check + tags: + - health +swagger: "2.0" diff --git a/go.mod b/go.mod index 7b32cc3..4426cba 100644 --- a/go.mod +++ b/go.mod @@ -4,9 +4,26 @@ go 1.23.0 toolchain go1.23.3 -require github.com/cilium/ebpf v0.19.0 +require ( + github.com/cilium/ebpf v0.19.0 + github.com/swaggo/http-swagger v1.3.4 + github.com/swaggo/swag v1.16.6 +) require ( + github.com/KyleBanks/depth v1.2.1 // indirect + github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/jsonreference v0.20.0 // indirect + github.com/go-openapi/spec v0.20.6 // indirect + github.com/go-openapi/swag v0.19.15 // indirect github.com/google/go-cmp v0.7.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/mailru/easyjson v0.7.6 // indirect + github.com/swaggo/files v1.0.1 // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/net v0.38.0 // indirect + golang.org/x/sync v0.12.0 // indirect golang.org/x/sys v0.31.0 // indirect + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index 8e5ffcd..44d8625 100644 --- a/go.sum +++ b/go.sum @@ -1,26 +1,108 @@ +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/cilium/ebpf v0.19.0 h1:Ro/rE64RmFBeA9FGjcTc+KmCeY6jXmryu6FfnzPRIao= github.com/cilium/ebpf v0.19.0/go.mod h1:fLCgMo3l8tZmAdM3B2XqdFzXBpwkcSTroaVqN08OWVY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA= +github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= +github.com/go-openapi/spec v0.20.6 h1:ich1RQ3WDbfoeTqTAb+5EIxNmpKVJZWBNah9RAT0jIQ= +github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-quicktest/qt v1.101.1-0.20240301121107-c6c8733fa1e6 h1:teYtXy9B7y5lHTp8V9KPxpYRAVA7dozigQcMiBust1s= github.com/go-quicktest/qt v1.101.1-0.20240301121107-c6c8733fa1e6/go.mod h1:p4lGIVX+8Wa6ZPNDvqcxq36XpUDLh42FLetFU7odllI= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/jsimonetti/rtnetlink/v2 v2.0.1 h1:xda7qaHDSVOsADNouv7ukSuicKZO7GgVUCXxpaIEIlM= github.com/jsimonetti/rtnetlink/v2 v2.0.1/go.mod h1:7MoNYNbb3UaDHtF8udiJo/RH6VsTKP1pqKLUTVCvToE= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= +github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= +github.com/swaggo/http-swagger v1.3.4 h1:q7t/XLx0n15H1Q9/tk3Y9L4n210XzJF5WtnDX64a5ww= +github.com/swaggo/http-swagger v1.3.4/go.mod h1:9dAh0unqMBAlbp1uE2Uc2mQTxNMU/ha4UbucIg1MFkQ= +github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI= +github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= -golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/api/handlers.go b/internal/api/handlers.go index 7ffee42..98395d4 100644 --- a/internal/api/handlers.go +++ b/internal/api/handlers.go @@ -1,309 +1,506 @@ +// Package api provides HTTP handlers for the eBPF monitoring system. +// +// @title eBPF Network Monitor API +// @description HTTP API for eBPF-based network connection and packet drop monitoring +// @version 1.0.0 +// @host localhost:8080 +// @BasePath / +// @contact.name API Support +// @contact.url https://github.com/srodi/ebpf-server/issues +// @contact.email support@example.com +// @license.name MIT +// @license.url https://github.com/srodi/ebpf-server/blob/main/LICENSE package api import ( + "context" "encoding/json" - "fmt" "net/http" "strconv" + "time" - "github.com/srodi/ebpf-server/internal/bpf" + "github.com/srodi/ebpf-server/internal/core" + "github.com/srodi/ebpf-server/internal/system" "github.com/srodi/ebpf-server/pkg/logger" ) -// ConnectionSummaryRequest defines the input parameters for the connection summary endpoint -type ConnectionSummaryRequest struct { - PID int `json:"pid,omitempty"` - Command string `json:"command,omitempty"` - ProcessName string `json:"process_name,omitempty"` - Seconds int `json:"duration"` -} +// Global system instance +var globalSystem *system.System -// ConnectionSummaryResponse defines the output structure for the connection summary endpoint -type ConnectionSummaryResponse struct { - Total int `json:"total_attempts"` - PID int `json:"pid,omitempty"` - Command string `json:"command,omitempty"` - Seconds int `json:"duration"` - Message string `json:"message"` +// Initialize sets up the API with the system instance. +func Initialize(sys *system.System) { + globalSystem = sys } -// ListConnectionsRequest defines the input parameters for the list connections endpoint -type ListConnectionsRequest struct { - PID *int `json:"pid,omitempty"` // Optional: Filter connections for specific Process ID - Limit *int `json:"limit,omitempty"` // Optional: Maximum connections to return per PID (default: 100, max: 1000) -} +// HandleHealth responds with system health information. +// +// @Summary Health check +// @Description Get the health status of the eBPF monitoring system +// @Tags health +// @Accept json +// @Produce json +// @Success 200 {object} map[string]interface{} "Health status" +// @Failure 503 {object} map[string]string "Service unavailable" +// @Router /health [get] +func HandleHealth(w http.ResponseWriter, r *http.Request) { + if globalSystem == nil { + http.Error(w, "System not initialized", http.StatusServiceUnavailable) + return + } -// ConnectionInfo represents connection event information -type ConnectionInfo struct { - PID uint32 `json:"pid"` - Command string `json:"command"` - Destination string `json:"destination"` - Protocol string `json:"protocol"` - ReturnCode int32 `json:"return_code"` - Timestamp string `json:"timestamp"` -} + health := map[string]interface{}{ + "status": "healthy", + "running": globalSystem.IsRunning(), + "time": time.Now().Format(time.RFC3339), + } -// ListConnectionsResponse defines the output format for the list connections endpoint -type ListConnectionsResponse struct { - TotalPIDs int `json:"total_pids"` - Connections map[string][]ConnectionInfo `json:"connections"` - Truncated bool `json:"truncated"` - Message string `json:"message"` + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(health); err != nil { + logger.Errorf("Error encoding health response: %v", err) + http.Error(w, "Internal server error", http.StatusInternalServerError) + } } -// ErrorResponse represents an API error response -type ErrorResponse struct { - Error string `json:"error"` - Message string `json:"message"` -} +// HandlePrograms returns the status of all eBPF programs. +// +// @Summary List eBPF programs +// @Description Get the status and information of all loaded eBPF programs +// @Tags programs +// @Accept json +// @Produce json +// @Success 200 {object} map[string]interface{} "List of eBPF programs" +// @Failure 500 {object} map[string]string "Internal server error" +// @Failure 503 {object} map[string]string "Service unavailable" +// @Router /api/programs [get] +func HandlePrograms(w http.ResponseWriter, r *http.Request) { + if globalSystem == nil { + http.Error(w, "System not initialized", http.StatusServiceUnavailable) + return + } + + programs := globalSystem.GetPrograms() -// writeJSONResponse writes a JSON response with the given status code -func writeJSONResponse(w http.ResponseWriter, statusCode int, data interface{}) { w.Header().Set("Content-Type", "application/json") - w.WriteHeader(statusCode) - if err := json.NewEncoder(w).Encode(data); err != nil { - logger.Errorf("Failed to encode JSON response: %v", err) + if err := json.NewEncoder(w).Encode(programs); err != nil { + logger.Errorf("Error encoding programs response: %v", err) + http.Error(w, "Internal server error", http.StatusInternalServerError) } } -// writeErrorResponse writes an error response -func writeErrorResponse(w http.ResponseWriter, statusCode int, message string) { - writeJSONResponse(w, statusCode, ErrorResponse{ - Error: http.StatusText(statusCode), - Message: message, - }) -} - -// HandleConnectionSummary handles the /api/connection-summary endpoint -func HandleConnectionSummary(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - writeErrorResponse(w, http.StatusMethodNotAllowed, "Only POST method is allowed") +// HandleEvents returns events matching query parameters. +// +// @Summary Query events +// @Description Get events filtered by type, PID, command, time range, and limit +// @Tags events +// @Accept json +// @Produce json +// @Param type query string false "Event type (connection, packet_drop)" +// @Param pid query int false "Process ID" +// @Param command query string false "Command name" +// @Param since query string false "Start time (RFC3339 format)" +// @Param until query string false "End time (RFC3339 format)" +// @Param limit query int false "Maximum number of events to return (default: 100)" +// @Success 200 {object} map[string]interface{} "Filtered events" +// @Failure 500 {object} map[string]string "Internal server error" +// @Failure 503 {object} map[string]string "Service unavailable" +// @Router /api/events [get] +func HandleEvents(w http.ResponseWriter, r *http.Request) { + if globalSystem == nil { + http.Error(w, "System not initialized", http.StatusServiceUnavailable) return } - var req ConnectionSummaryRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - writeErrorResponse(w, http.StatusBadRequest, "Invalid JSON request body") - return + // Parse query parameters + query := core.Query{} + + if eventType := r.URL.Query().Get("type"); eventType != "" { + query.EventType = eventType } - - // Handle compatibility between 'command' and 'process_name' fields - command := req.Command - if command == "" && req.ProcessName != "" { - command = req.ProcessName + + if pidStr := r.URL.Query().Get("pid"); pidStr != "" { + if pid, err := strconv.ParseUint(pidStr, 10, 32); err == nil { + query.PID = uint32(pid) + } + } + + if command := r.URL.Query().Get("command"); command != "" { + query.Command = command + } + + if sinceStr := r.URL.Query().Get("since"); sinceStr != "" { + if since, err := time.Parse(time.RFC3339, sinceStr); err == nil { + query.Since = since + } + } + + if untilStr := r.URL.Query().Get("until"); untilStr != "" { + if until, err := time.Parse(time.RFC3339, untilStr); err == nil { + query.Until = until + } + } + + if limitStr := r.URL.Query().Get("limit"); limitStr != "" { + if limit, err := strconv.Atoi(limitStr); err == nil && limit > 0 { + query.Limit = limit + } + } + + // Default limit to prevent overwhelming responses + if query.Limit == 0 { + query.Limit = 100 } - // Validate input - if req.Seconds <= 0 { - writeErrorResponse(w, http.StatusBadRequest, "duration must be positive") + ctx := context.Background() + events, err := globalSystem.QueryEvents(ctx, query) + if err != nil { + logger.Errorf("Error querying events: %v", err) + http.Error(w, "Internal server error", http.StatusInternalServerError) return } - if req.Seconds > 3600 { - writeErrorResponse(w, http.StatusBadRequest, "duration cannot exceed 3600 seconds") - return + + response := map[string]interface{}{ + "events": events, + "count": len(events), + "query": query, } - if req.PID != 0 && command != "" { - writeErrorResponse(w, http.StatusBadRequest, "cannot specify both PID and command") - return + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(response); err != nil { + logger.Errorf("Error encoding events response: %v", err) + http.Error(w, "Internal server error", http.StatusInternalServerError) } - if req.PID == 0 && command == "" { - writeErrorResponse(w, http.StatusBadRequest, "must specify either PID or command") +} + +// HandleConnectionSummary provides connection event summaries. +// +// @Summary Get connection statistics +// @Description Get count of connection events filtered by PID, command, and time window +// @Tags connections +// @Accept json +// @Produce json +// @Param pid query int false "Process ID (GET only)" +// @Param command query string false "Command name (GET only)" +// @Param duration_seconds query int false "Duration in seconds (GET only, default: 60)" +// @Param request body ConnectionSummaryRequest false "Connection summary request (POST only)" +// @Success 200 {object} ConnectionSummaryResponse "Connection statistics" +// @Failure 400 {object} map[string]string "Bad request" +// @Failure 500 {object} map[string]string "Internal server error" +// @Failure 503 {object} map[string]string "Service unavailable" +// @Router /api/connection-summary [get] +// @Router /api/connection-summary [post] +func HandleConnectionSummary(w http.ResponseWriter, r *http.Request) { + if globalSystem == nil { + http.Error(w, "System not initialized", http.StatusServiceUnavailable) return } - // Get connection summary data - var total int - var monitoredPID int - var monitoredCommand string + // Parse request body for POST requests + var request struct { + PID uint32 `json:"pid"` + Command string `json:"command"` + Duration int `json:"duration_seconds"` + } - if command != "" { - total = bpf.GetConnectionSummary(0, command, req.Seconds) - monitoredCommand = command - logger.Debugf("Connection summary for command '%s': %d attempts in %d seconds", - command, total, req.Seconds) + if r.Method == "POST" { + if err := json.NewDecoder(r.Body).Decode(&request); err != nil { + http.Error(w, "Invalid JSON", http.StatusBadRequest) + return + } } else { - total = bpf.GetConnectionSummary(uint32(req.PID), "", req.Seconds) - monitoredPID = req.PID - logger.Debugf("Connection summary for PID %d: %d attempts in %d seconds", - req.PID, total, req.Seconds) + // Handle GET request with query parameters + if pidStr := r.URL.Query().Get("pid"); pidStr != "" { + if pid, err := strconv.ParseUint(pidStr, 10, 32); err == nil { + request.PID = uint32(pid) + } + } + request.Command = r.URL.Query().Get("command") + if durationStr := r.URL.Query().Get("duration_seconds"); durationStr != "" { + if duration, err := strconv.Atoi(durationStr); err == nil { + request.Duration = duration + } + } } - // Create human-readable message - var message string - if monitoredCommand != "" { - message = fmt.Sprintf("Found %d connection attempts from command '%s' over %d seconds", - total, monitoredCommand, req.Seconds) - } else { - message = fmt.Sprintf("Found %d connection attempts from PID %d over %d seconds", - total, monitoredPID, req.Seconds) + // Default duration to 60 seconds + if request.Duration == 0 { + request.Duration = 60 } - response := ConnectionSummaryResponse{ - Total: total, - PID: monitoredPID, - Command: monitoredCommand, - Seconds: req.Seconds, - Message: message, + // Build query + query := core.Query{ + EventType: "connection", + PID: request.PID, + Command: request.Command, + Since: time.Now().Add(-time.Duration(request.Duration) * time.Second), } - writeJSONResponse(w, http.StatusOK, response) -} + ctx := context.Background() + count, err := globalSystem.CountEvents(ctx, query) + if err != nil { + logger.Errorf("Error counting connection events: %v", err) + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } -// HandleListConnections handles the /api/list-connections endpoint -func HandleListConnections(w http.ResponseWriter, r *http.Request) { - if r.Method == http.MethodGet { - // Handle GET request with query parameters - handleListConnectionsGET(w, r) - } else if r.Method == http.MethodPost { - // Handle POST request with JSON body - handleListConnectionsPOST(w, r) - } else { - writeErrorResponse(w, http.StatusMethodNotAllowed, "Only GET and POST methods are allowed") + response := map[string]interface{}{ + "count": count, + "pid": request.PID, + "command": request.Command, + "duration_seconds": request.Duration, + "query_time": time.Now().Format(time.RFC3339), + } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(response); err != nil { + logger.Errorf("Error encoding connection summary response: %v", err) + http.Error(w, "Internal server error", http.StatusInternalServerError) } } -func handleListConnectionsGET(w http.ResponseWriter, r *http.Request) { - var req ListConnectionsRequest +// HandlePacketDropSummary provides packet drop event summaries. +// +// @Summary Get packet drop statistics +// @Description Get count of packet drop events filtered by PID, command, and time window +// @Tags packet_drops +// @Accept json +// @Produce json +// @Param pid query int false "Process ID (GET only)" +// @Param command query string false "Command name (GET only)" +// @Param duration_seconds query int false "Duration in seconds (GET only, default: 60)" +// @Param request body PacketDropSummaryRequest false "Packet drop summary request (POST only)" +// @Success 200 {object} PacketDropSummaryResponse "Packet drop statistics" +// @Failure 400 {object} map[string]string "Bad request" +// @Failure 500 {object} map[string]string "Internal server error" +// @Failure 503 {object} map[string]string "Service unavailable" +// @Router /api/packet-drop-summary [get] +// @Router /api/packet-drop-summary [post] +func HandlePacketDropSummary(w http.ResponseWriter, r *http.Request) { + if globalSystem == nil { + http.Error(w, "System not initialized", http.StatusServiceUnavailable) + return + } - // Parse query parameters - if pidStr := r.URL.Query().Get("pid"); pidStr != "" { - if pid, err := strconv.Atoi(pidStr); err != nil { - writeErrorResponse(w, http.StatusBadRequest, "Invalid PID parameter") - return - } else { - req.PID = &pid - } + // Parse request body for POST requests + var request struct { + PID uint32 `json:"pid"` + Command string `json:"command"` + Duration int `json:"duration_seconds"` } - if limitStr := r.URL.Query().Get("limit"); limitStr != "" { - if limit, err := strconv.Atoi(limitStr); err != nil { - writeErrorResponse(w, http.StatusBadRequest, "Invalid limit parameter") + if r.Method == "POST" { + if err := json.NewDecoder(r.Body).Decode(&request); err != nil { + http.Error(w, "Invalid JSON", http.StatusBadRequest) return - } else { - req.Limit = &limit + } + } else { + // Handle GET request with query parameters + if pidStr := r.URL.Query().Get("pid"); pidStr != "" { + if pid, err := strconv.ParseUint(pidStr, 10, 32); err == nil { + request.PID = uint32(pid) + } + } + request.Command = r.URL.Query().Get("command") + if durationStr := r.URL.Query().Get("duration_seconds"); durationStr != "" { + if duration, err := strconv.Atoi(durationStr); err == nil { + request.Duration = duration + } } } - processListConnectionsRequest(w, req) -} + // Default duration to 60 seconds + if request.Duration == 0 { + request.Duration = 60 + } + + // Build query + query := core.Query{ + EventType: "packet_drop", + PID: request.PID, + Command: request.Command, + Since: time.Now().Add(-time.Duration(request.Duration) * time.Second), + } -func handleListConnectionsPOST(w http.ResponseWriter, r *http.Request) { - var req ListConnectionsRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - writeErrorResponse(w, http.StatusBadRequest, "Invalid JSON request body") + ctx := context.Background() + count, err := globalSystem.CountEvents(ctx, query) + if err != nil { + logger.Errorf("Error counting packet drop events: %v", err) + http.Error(w, "Internal server error", http.StatusInternalServerError) return } - processListConnectionsRequest(w, req) + response := map[string]interface{}{ + "count": count, + "pid": request.PID, + "command": request.Command, + "duration_seconds": request.Duration, + "query_time": time.Now().Format(time.RFC3339), + } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(response); err != nil { + logger.Errorf("Error encoding packet drop summary response: %v", err) + http.Error(w, "Internal server error", http.StatusInternalServerError) + } } -func processListConnectionsRequest(w http.ResponseWriter, req ListConnectionsRequest) { - // Validate input - if req.Limit != nil && *req.Limit > 1000 { - writeErrorResponse(w, http.StatusBadRequest, "limit cannot exceed 1000") +// HandleListConnections returns recent connection events. +// +// @Summary List connection events +// @Description Get recent connection events grouped by PID +// @Tags connections +// @Accept json +// @Produce json +// @Success 200 {object} ConnectionListResponse "Connection events" +// @Failure 500 {object} map[string]string "Internal server error" +// @Failure 503 {object} map[string]string "Service unavailable" +// @Router /api/list-connections [get] +func HandleListConnections(w http.ResponseWriter, r *http.Request) { + logger.Debugf("🌐 HTTP REQUEST: %s %s from %s", r.Method, r.URL.Path, r.RemoteAddr) + + if globalSystem == nil { + http.Error(w, "System not initialized", http.StatusServiceUnavailable) return } - limitValue := 100 - if req.Limit != nil { - limitValue = *req.Limit + query := core.Query{ + EventType: "connection", + Limit: 100, + Since: time.Now().Add(-1 * time.Hour), // Last hour by default } - // Get all connections from the eBPF loader - allConnections := bpf.GetAllConnections() + ctx := context.Background() + events, err := globalSystem.QueryEvents(ctx, query) + if err != nil { + logger.Errorf("Error querying connection events: %v", err) + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } - result := struct { - TotalPIDs int - Connections map[string][]ConnectionInfo - Truncated bool - }{ - Connections: make(map[string][]ConnectionInfo), - Truncated: false, + // Group by PID for compatibility + eventsByPID := make(map[uint32][]core.Event) + for _, event := range events { + pid := event.PID() + eventsByPID[pid] = append(eventsByPID[pid], event) } - pidCount := 0 - for connectionPID, events := range allConnections { - // If PID filter is specified, skip non-matching PIDs - if req.PID != nil && connectionPID != uint32(*req.PID) { - continue - } + response := map[string]interface{}{ + "total_pids": len(eventsByPID), + "total_events": len(events), + "events_by_pid": eventsByPID, + "query_time": time.Now().Format(time.RFC3339), + } - pidCount++ - var connections []ConnectionInfo - pidStr := fmt.Sprintf("%d", connectionPID) + logger.Debugf("🌐 HTTP RESPONSE: connections query returned %d events across %d PIDs", len(events), len(eventsByPID)) - // Limit connections per PID - eventCount := 0 - for _, event := range events { - if eventCount >= limitValue { - result.Truncated = true - break - } + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(response); err != nil { + logger.Errorf("Error encoding list connections response: %v", err) + http.Error(w, "Internal server error", http.StatusInternalServerError) + } +} - connections = append(connections, ConnectionInfo{ - PID: event.PID, - Command: event.GetCommand(), - Destination: event.GetDestination(), - Protocol: event.GetProtocol(), - ReturnCode: event.Ret, - Timestamp: event.GetWallClockTime().Format("2006-01-02T15:04:05Z"), - }) - eventCount++ - } +// HandleListPacketDrops returns recent packet drop events. +// +// @Summary List packet drop events +// @Description Get recent packet drop events grouped by PID +// @Tags packet_drops +// @Accept json +// @Produce json +// @Success 200 {object} PacketDropListResponse "Packet drop events" +// @Failure 500 {object} map[string]string "Internal server error" +// @Failure 503 {object} map[string]string "Service unavailable" +// @Router /api/list-packet-drops [get] +func HandleListPacketDrops(w http.ResponseWriter, r *http.Request) { + logger.Debugf("🌐 HTTP REQUEST: %s %s from %s", r.Method, r.URL.Path, r.RemoteAddr) + + if globalSystem == nil { + http.Error(w, "System not initialized", http.StatusServiceUnavailable) + return + } - if len(connections) > 0 { - result.Connections[pidStr] = connections - } + query := core.Query{ + EventType: "packet_drop", + Limit: 100, + Since: time.Now().Add(-1 * time.Hour), // Last hour by default } - result.TotalPIDs = pidCount + ctx := context.Background() + events, err := globalSystem.QueryEvents(ctx, query) + if err != nil { + logger.Errorf("Error querying packet drop events: %v", err) + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } - logger.Debugf("List connections result: %d PIDs", result.TotalPIDs) + // Group by PID for compatibility + eventsByPID := make(map[uint32][]core.Event) + for _, event := range events { + pid := event.PID() + eventsByPID[pid] = append(eventsByPID[pid], event) + } - // Create human-readable message - var message string - if req.PID != nil { - totalConns := 0 - for _, conns := range result.Connections { - totalConns += len(conns) - } - message = fmt.Sprintf("Found %d connections for PID %d", totalConns, *req.PID) - } else { - totalConns := 0 - for _, conns := range result.Connections { - totalConns += len(conns) - } - message = fmt.Sprintf("Found %d total connections across %d processes", - totalConns, result.TotalPIDs) - if result.Truncated { - message += " (results truncated due to limit)" - } + response := map[string]interface{}{ + "total_pids": len(eventsByPID), + "total_events": len(events), + "events_by_pid": eventsByPID, + "query_time": time.Now().Format(time.RFC3339), } - response := ListConnectionsResponse{ - TotalPIDs: result.TotalPIDs, - Connections: result.Connections, - Truncated: result.Truncated, - Message: message, + logger.Debugf("🌐 HTTP RESPONSE: packet drops query returned %d events across %d PIDs", len(events), len(eventsByPID)) + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(response); err != nil { + logger.Errorf("Error encoding list packet drops response: %v", err) + http.Error(w, "Internal server error", http.StatusInternalServerError) } +} - logger.Debugf("List connections result: %d PIDs, %s", response.TotalPIDs, response.Message) +// Swagger models for request/response documentation - writeJSONResponse(w, http.StatusOK, response) +// ConnectionSummaryRequest represents the request body for connection summary +type ConnectionSummaryRequest struct { + PID uint32 `json:"pid" example:"1234"` // Process ID + Command string `json:"command" example:"curl"` // Command name + Duration int `json:"duration_seconds" example:"60"` // Duration in seconds } -// HandleHealth provides a simple health check endpoint -func HandleHealth(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodGet { - writeErrorResponse(w, http.StatusMethodNotAllowed, "Only GET method is allowed") - return - } +// ConnectionSummaryResponse represents the response for connection summary +type ConnectionSummaryResponse struct { + Count int `json:"count" example:"5"` // Number of connection events + PID uint32 `json:"pid" example:"1234"` // Process ID + Command string `json:"command" example:"curl"` // Command name + DurationSeconds int `json:"duration_seconds" example:"60"` // Duration in seconds + QueryTime string `json:"query_time" example:"2023-01-01T12:00:00Z"` // Query timestamp +} - health := map[string]string{ - "status": "healthy", - "service": "ebpf-server", - "version": "v1.0.0", - } +// PacketDropSummaryRequest represents the request body for packet drop summary +type PacketDropSummaryRequest struct { + PID uint32 `json:"pid" example:"1234"` // Process ID + Command string `json:"command" example:"nginx"` // Command name + Duration int `json:"duration_seconds" example:"60"` // Duration in seconds +} + +// PacketDropSummaryResponse represents the response for packet drop summary +type PacketDropSummaryResponse struct { + Count int `json:"count" example:"3"` // Number of packet drop events + PID uint32 `json:"pid" example:"1234"` // Process ID + Command string `json:"command" example:"nginx"` // Command name + DurationSeconds int `json:"duration_seconds" example:"60"` // Duration in seconds + QueryTime string `json:"query_time" example:"2023-01-01T12:00:00Z"` // Query timestamp +} + +// ConnectionListResponse represents the response for listing connections +type ConnectionListResponse struct { + TotalPIDs int `json:"total_pids" example:"3"` // Number of unique PIDs + TotalEvents int `json:"total_events" example:"10"` // Total number of events + EventsByPID map[uint32][]core.Event `json:"events_by_pid"` // Events grouped by PID + QueryTime string `json:"query_time" example:"2023-01-01T12:00:00Z"` // Query timestamp +} - writeJSONResponse(w, http.StatusOK, health) +// PacketDropListResponse represents the response for listing packet drops +type PacketDropListResponse struct { + TotalPIDs int `json:"total_pids" example:"2"` // Number of unique PIDs + TotalEvents int `json:"total_events" example:"7"` // Total number of events + EventsByPID map[uint32][]core.Event `json:"events_by_pid"` // Events grouped by PID + QueryTime string `json:"query_time" example:"2023-01-01T12:00:00Z"` // Query timestamp } diff --git a/internal/api/handlers_test.go b/internal/api/handlers_test.go index a23d215..df661cb 100644 --- a/internal/api/handlers_test.go +++ b/internal/api/handlers_test.go @@ -8,274 +8,317 @@ import ( "testing" ) -func TestHandleHealth(t *testing.T) { - req, err := http.NewRequest("GET", "/health", nil) - if err != nil { - t.Fatal(err) +// TestHandleHealthWithoutSystem tests the health endpoint when system is not initialized +func TestHandleHealthWithoutSystem(t *testing.T) { + // Ensure no system is initialized + originalSystem := globalSystem + globalSystem = nil + defer func() { globalSystem = originalSystem }() + + req := httptest.NewRequest("GET", "/health", nil) + w := httptest.NewRecorder() + + HandleHealth(w, req) + + if w.Code != http.StatusServiceUnavailable { + t.Errorf("expected status %d, got %d", http.StatusServiceUnavailable, w.Code) } - - rr := httptest.NewRecorder() - handler := http.HandlerFunc(HandleHealth) - - handler.ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got %v want %v", - status, http.StatusOK) + + body := w.Body.String() + if body != "System not initialized\n" { + t.Errorf("expected error message 'System not initialized', got %s", body) } +} - var response map[string]string - if err := json.Unmarshal(rr.Body.Bytes(), &response); err != nil { - t.Fatalf("Failed to parse JSON response: %v", err) +// TestHandleProgramsWithoutSystem tests the programs endpoint when system is not initialized +func TestHandleProgramsWithoutSystem(t *testing.T) { + originalSystem := globalSystem + globalSystem = nil + defer func() { globalSystem = originalSystem }() + + req := httptest.NewRequest("GET", "/api/programs", nil) + w := httptest.NewRecorder() + + HandlePrograms(w, req) + + if w.Code != http.StatusServiceUnavailable { + t.Errorf("expected status %d, got %d", http.StatusServiceUnavailable, w.Code) } +} - if response["status"] != "healthy" { - t.Errorf("Expected status 'healthy', got '%s'", response["status"]) +// TestHandleEventsWithoutSystem tests the events endpoint when system is not initialized +func TestHandleEventsWithoutSystem(t *testing.T) { + originalSystem := globalSystem + globalSystem = nil + defer func() { globalSystem = originalSystem }() + + req := httptest.NewRequest("GET", "/api/events", nil) + w := httptest.NewRecorder() + + HandleEvents(w, req) + + if w.Code != http.StatusServiceUnavailable { + t.Errorf("expected status %d, got %d", http.StatusServiceUnavailable, w.Code) } } -func TestHandleHealthWrongMethod(t *testing.T) { - req, err := http.NewRequest("POST", "/health", nil) - if err != nil { - t.Fatal(err) +// TestHandleConnectionSummaryWithoutSystem tests connection summary without system +func TestHandleConnectionSummaryWithoutSystem(t *testing.T) { + originalSystem := globalSystem + globalSystem = nil + defer func() { globalSystem = originalSystem }() + + req := httptest.NewRequest("GET", "/api/connection-summary", nil) + w := httptest.NewRecorder() + + HandleConnectionSummary(w, req) + + if w.Code != http.StatusServiceUnavailable { + t.Errorf("expected status %d, got %d", http.StatusServiceUnavailable, w.Code) } +} - rr := httptest.NewRecorder() - handler := http.HandlerFunc(HandleHealth) - - handler.ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusMethodNotAllowed { - t.Errorf("handler returned wrong status code: got %v want %v", - status, http.StatusMethodNotAllowed) +// TestHandlePacketDropSummaryWithoutSystem tests packet drop summary without system +func TestHandlePacketDropSummaryWithoutSystem(t *testing.T) { + originalSystem := globalSystem + globalSystem = nil + defer func() { globalSystem = originalSystem }() + + req := httptest.NewRequest("GET", "/api/packet-drop-summary", nil) + w := httptest.NewRecorder() + + HandlePacketDropSummary(w, req) + + if w.Code != http.StatusServiceUnavailable { + t.Errorf("expected status %d, got %d", http.StatusServiceUnavailable, w.Code) } } -func TestHandleConnectionSummaryValidation(t *testing.T) { - tests := []struct { - name string - request ConnectionSummaryRequest - wantCode int - }{ - { - name: "valid request with PID", - request: ConnectionSummaryRequest{ - PID: 1234, - Seconds: 60, - }, - wantCode: http.StatusOK, - }, - { - name: "valid request with command", - request: ConnectionSummaryRequest{ - Command: "curl", - Seconds: 30, - }, - wantCode: http.StatusOK, - }, - { - name: "invalid duration - zero", - request: ConnectionSummaryRequest{ - PID: 1234, - Seconds: 0, - }, - wantCode: http.StatusBadRequest, - }, - { - name: "invalid duration - too long", - request: ConnectionSummaryRequest{ - PID: 1234, - Seconds: 4000, - }, - wantCode: http.StatusBadRequest, - }, - { - name: "both PID and command specified", - request: ConnectionSummaryRequest{ - PID: 1234, - Command: "curl", - Seconds: 60, - }, - wantCode: http.StatusBadRequest, - }, - { - name: "neither PID nor command specified", - request: ConnectionSummaryRequest{ - Seconds: 60, - }, - wantCode: http.StatusBadRequest, - }, +// TestHandleListConnectionsWithoutSystem tests list connections without system +func TestHandleListConnectionsWithoutSystem(t *testing.T) { + originalSystem := globalSystem + globalSystem = nil + defer func() { globalSystem = originalSystem }() + + req := httptest.NewRequest("GET", "/api/list-connections", nil) + w := httptest.NewRecorder() + + HandleListConnections(w, req) + + if w.Code != http.StatusServiceUnavailable { + t.Errorf("expected status %d, got %d", http.StatusServiceUnavailable, w.Code) } +} - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - reqBody, err := json.Marshal(tt.request) - if err != nil { - t.Fatal(err) - } - - req, err := http.NewRequest("POST", "/api/connection-summary", bytes.NewBuffer(reqBody)) - if err != nil { - t.Fatal(err) - } - req.Header.Set("Content-Type", "application/json") - - rr := httptest.NewRecorder() - handler := http.HandlerFunc(HandleConnectionSummary) - - handler.ServeHTTP(rr, req) - - if status := rr.Code; status != tt.wantCode { - t.Errorf("handler returned wrong status code: got %v want %v", - status, tt.wantCode) - } - }) +// TestHandleListPacketDropsWithoutSystem tests list packet drops without system +func TestHandleListPacketDropsWithoutSystem(t *testing.T) { + originalSystem := globalSystem + globalSystem = nil + defer func() { globalSystem = originalSystem }() + + req := httptest.NewRequest("GET", "/api/list-packet-drops", nil) + w := httptest.NewRecorder() + + HandleListPacketDrops(w, req) + + if w.Code != http.StatusServiceUnavailable { + t.Errorf("expected status %d, got %d", http.StatusServiceUnavailable, w.Code) } } -func TestHandleListConnectionsGET(t *testing.T) { - req, err := http.NewRequest("GET", "/api/list-connections", nil) - if err != nil { - t.Fatal(err) +// TestHandleConnectionSummaryPOSTInvalidJSON tests POST with invalid JSON +func TestHandleConnectionSummaryPOSTInvalidJSON(t *testing.T) { + originalSystem := globalSystem + globalSystem = nil + defer func() { globalSystem = originalSystem }() + + req := httptest.NewRequest("POST", "/api/connection-summary", bytes.NewReader([]byte("invalid json"))) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + + HandleConnectionSummary(w, req) + + // Should fail due to no system first, but if we had a system it would fail due to invalid JSON + if w.Code != http.StatusServiceUnavailable { + t.Errorf("expected status %d, got %d", http.StatusServiceUnavailable, w.Code) } +} - rr := httptest.NewRecorder() - handler := http.HandlerFunc(HandleListConnections) - - handler.ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got %v want %v", - status, http.StatusOK) +// TestHandlePacketDropSummaryPOSTInvalidJSON tests POST with invalid JSON +func TestHandlePacketDropSummaryPOSTInvalidJSON(t *testing.T) { + originalSystem := globalSystem + globalSystem = nil + defer func() { globalSystem = originalSystem }() + + req := httptest.NewRequest("POST", "/api/packet-drop-summary", bytes.NewReader([]byte("invalid json"))) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + + HandlePacketDropSummary(w, req) + + if w.Code != http.StatusServiceUnavailable { + t.Errorf("expected status %d, got %d", http.StatusServiceUnavailable, w.Code) } +} - var response ListConnectionsResponse - if err := json.Unmarshal(rr.Body.Bytes(), &response); err != nil { - t.Fatalf("Failed to parse JSON response: %v", err) +// TestHandleEventsQueryParameterParsing tests query parameter parsing +func TestHandleEventsQueryParameterParsing(t *testing.T) { + originalSystem := globalSystem + globalSystem = nil + defer func() { globalSystem = originalSystem }() + + // Test that URL query parameters are parsed correctly, even without a system + // The handler should parse the parameters before checking for system availability + testCases := []struct { + name string + params string + }{ + {"type parameter", "?type=connection"}, + {"pid parameter", "?pid=1234"}, + {"command parameter", "?command=curl"}, + {"limit parameter", "?limit=50"}, + {"since parameter", "?since=2023-01-01T12:00:00Z"}, + {"until parameter", "?until=2023-01-01T13:00:00Z"}, + {"multiple parameters", "?type=connection&pid=1234&limit=10"}, } - - // Should have a valid response structure - if response.Connections == nil { - t.Error("Expected connections map to be initialized") + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + req := httptest.NewRequest("GET", "/api/events"+tc.params, nil) + w := httptest.NewRecorder() + + HandleEvents(w, req) + + // Should fail due to no system, but the URL parsing should work + if w.Code != http.StatusServiceUnavailable { + t.Errorf("expected status %d, got %d", http.StatusServiceUnavailable, w.Code) + } + }) } } -func TestHandleListConnectionsGETWithParams(t *testing.T) { - req, err := http.NewRequest("GET", "/api/list-connections?pid=1234&limit=50", nil) - if err != nil { - t.Fatal(err) +// TestHandleConnectionSummaryGETParameterParsing tests GET parameter parsing +func TestHandleConnectionSummaryGETParameterParsing(t *testing.T) { + originalSystem := globalSystem + globalSystem = nil + defer func() { globalSystem = originalSystem }() + + testCases := []struct { + name string + params string + }{ + {"pid parameter", "?pid=1234"}, + {"command parameter", "?command=curl"}, + {"duration parameter", "?duration_seconds=30"}, + {"all parameters", "?pid=1234&command=curl&duration_seconds=120"}, } - - rr := httptest.NewRecorder() - handler := http.HandlerFunc(HandleListConnections) - - handler.ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got %v want %v", - status, http.StatusOK) + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + req := httptest.NewRequest("GET", "/api/connection-summary"+tc.params, nil) + w := httptest.NewRecorder() + + HandleConnectionSummary(w, req) + + if w.Code != http.StatusServiceUnavailable { + t.Errorf("expected status %d, got %d", http.StatusServiceUnavailable, w.Code) + } + }) } } -func TestHandleListConnectionsPOST(t *testing.T) { - reqData := ListConnectionsRequest{ - Limit: func(i int) *int { return &i }(100), +// TestHandleConnectionSummaryPOSTValidJSON tests POST with valid JSON structure +func TestHandleConnectionSummaryPOSTValidJSON(t *testing.T) { + originalSystem := globalSystem + globalSystem = nil + defer func() { globalSystem = originalSystem }() + + requestBody := map[string]interface{}{ + "pid": 1234, + "command": "curl", + "duration_seconds": 60, } - - reqBody, err := json.Marshal(reqData) - if err != nil { - t.Fatal(err) - } - - req, err := http.NewRequest("POST", "/api/list-connections", bytes.NewBuffer(reqBody)) + + jsonBody, err := json.Marshal(requestBody) if err != nil { - t.Fatal(err) + t.Fatalf("failed to marshal JSON: %v", err) } + + req := httptest.NewRequest("POST", "/api/connection-summary", bytes.NewReader(jsonBody)) req.Header.Set("Content-Type", "application/json") - - rr := httptest.NewRecorder() - handler := http.HandlerFunc(HandleListConnections) - - handler.ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got %v want %v", - status, http.StatusOK) + w := httptest.NewRecorder() + + HandleConnectionSummary(w, req) + + // Should fail due to no system, but JSON parsing should work + if w.Code != http.StatusServiceUnavailable { + t.Errorf("expected status %d, got %d", http.StatusServiceUnavailable, w.Code) } } -func TestHandleListConnectionsLimitValidation(t *testing.T) { - reqData := ListConnectionsRequest{ - Limit: func(i int) *int { return &i }(1500), // Over the limit +// TestSwaggerModelStructures tests that our swagger model structs are well-formed +func TestSwaggerModelStructures(t *testing.T) { + // Test ConnectionSummaryRequest + req := ConnectionSummaryRequest{ + PID: 1234, + Command: "curl", + Duration: 60, } - - reqBody, err := json.Marshal(reqData) - if err != nil { - t.Fatal(err) + + if req.PID != 1234 { + t.Errorf("expected PID 1234, got %d", req.PID) } - - req, err := http.NewRequest("POST", "/api/list-connections", bytes.NewBuffer(reqBody)) + + // Test marshaling to JSON + jsonData, err := json.Marshal(req) if err != nil { - t.Fatal(err) + t.Errorf("failed to marshal ConnectionSummaryRequest: %v", err) } - req.Header.Set("Content-Type", "application/json") - - rr := httptest.NewRecorder() - handler := http.HandlerFunc(HandleListConnections) - - handler.ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusBadRequest { - t.Errorf("handler returned wrong status code: got %v want %v", - status, http.StatusBadRequest) - } -} - -func TestWriteJSONResponse(t *testing.T) { - testData := map[string]string{"test": "value"} - - rr := httptest.NewRecorder() - writeJSONResponse(rr, http.StatusOK, testData) - - if status := rr.Code; status != http.StatusOK { - t.Errorf("writeJSONResponse returned wrong status code: got %v want %v", - status, http.StatusOK) + + // Test unmarshaling from JSON + var decoded ConnectionSummaryRequest + if err := json.Unmarshal(jsonData, &decoded); err != nil { + t.Errorf("failed to unmarshal ConnectionSummaryRequest: %v", err) } - - if contentType := rr.Header().Get("Content-Type"); contentType != "application/json" { - t.Errorf("writeJSONResponse returned wrong content type: got %v want %v", - contentType, "application/json") + + if decoded.Command != "curl" { + t.Errorf("expected command 'curl', got %s", decoded.Command) } - - var response map[string]string - if err := json.Unmarshal(rr.Body.Bytes(), &response); err != nil { - t.Fatalf("Failed to parse JSON response: %v", err) + + // Test ConnectionSummaryResponse + resp := ConnectionSummaryResponse{ + Count: 5, + PID: 1234, + Command: "curl", + DurationSeconds: 60, + QueryTime: "2023-01-01T12:00:00Z", } - - if response["test"] != "value" { - t.Errorf("Expected test=value, got test=%s", response["test"]) + + if resp.Count != 5 { + t.Errorf("expected count 5, got %d", resp.Count) } -} - -func TestWriteErrorResponse(t *testing.T) { - rr := httptest.NewRecorder() - writeErrorResponse(rr, http.StatusBadRequest, "test error message") - - if status := rr.Code; status != http.StatusBadRequest { - t.Errorf("writeErrorResponse returned wrong status code: got %v want %v", - status, http.StatusBadRequest) + + // Test PacketDropSummaryRequest + dropReq := PacketDropSummaryRequest{ + PID: 5678, + Command: "nginx", + Duration: 120, } - - var response ErrorResponse - if err := json.Unmarshal(rr.Body.Bytes(), &response); err != nil { - t.Fatalf("Failed to parse JSON response: %v", err) + + if dropReq.PID != 5678 { + t.Errorf("expected PID 5678, got %d", dropReq.PID) } - - if response.Message != "test error message" { - t.Errorf("Expected message='test error message', got message='%s'", response.Message) + + // Test PacketDropSummaryResponse + dropResp := PacketDropSummaryResponse{ + Count: 3, + PID: 5678, + Command: "nginx", + DurationSeconds: 120, + QueryTime: "2023-01-01T12:00:00Z", } - - if response.Error != "Bad Request" { - t.Errorf("Expected error='Bad Request', got error='%s'", response.Error) + + if dropResp.Count != 3 { + t.Errorf("expected count 3, got %d", dropResp.Count) } } diff --git a/internal/bpf/loader.go b/internal/bpf/loader.go deleted file mode 100644 index d7105fd..0000000 --- a/internal/bpf/loader.go +++ /dev/null @@ -1,370 +0,0 @@ -package bpf - -import ( - "context" - "encoding/binary" - "os" - "strconv" - "strings" - "sync" - "time" - - "github.com/cilium/ebpf" - "github.com/cilium/ebpf/link" - "github.com/cilium/ebpf/ringbuf" - "github.com/cilium/ebpf/rlimit" - "github.com/srodi/ebpf-server/pkg/logger" -) - -var ( - objs struct { - Events *ebpf.Map - } - links []link.Link - - // Connection tracking - connectionsMu sync.RWMutex - connections = make(map[uint32][]Event) // PID -> []Event - - // Ring buffer reader - reader *ringbuf.Reader - - // Boot time for converting eBPF timestamps to wall clock time - systemBootTime time.Time - - // Context for graceful shutdown of event processing - eventCtx context.Context - eventCancel context.CancelFunc - eventDone chan struct{} -) - -// IsAvailable checks if eBPF is available on the current system -func IsAvailable() bool { - // On macOS, eBPF is not available - if strings.Contains(strings.ToLower(os.Getenv("GOOS")), "darwin") { - return false - } - - // Try to remove memory limit for eBPF - this will fail if not supported - if err := rlimit.RemoveMemlock(); err != nil { - return false - } - - // Check if we can access /sys/fs/bpf (BPF filesystem) - if _, err := os.Stat("/sys/fs/bpf"); os.IsNotExist(err) { - return false - } - - return true -} - -// calculateBootTimeOffset calculates the system boot time for timestamp conversion -func calculateBootTimeOffset() { - // Read system uptime from /proc/uptime - data, err := os.ReadFile("/proc/uptime") - if err != nil { - logger.Debugf("Could not read /proc/uptime: %v", err) - systemBootTime = time.Now() // Fallback to current time - return - } - - // Parse uptime (first number is seconds since boot) - uptimeStr := strings.Fields(string(data))[0] - uptime, err := strconv.ParseFloat(uptimeStr, 64) - if err != nil { - logger.Debugf("Could not parse uptime: %v", err) - systemBootTime = time.Now() - return - } - - // Calculate boot time - systemBootTime = time.Now().Add(-time.Duration(uptime * float64(time.Second))) - - logger.Infof("System boot time calculated: %s", systemBootTime.Format("2006-01-02 15:04:05")) -} - -// GetSystemBootTime returns the calculated system boot time -func GetSystemBootTime() time.Time { - return systemBootTime -} - -func LoadAndAttach() error { - if err := rlimit.RemoveMemlock(); err != nil { - return err - } - - // Calculate boot time offset for timestamp conversion - calculateBootTimeOffset() - - // Initialize context for graceful shutdown - eventCtx, eventCancel = context.WithCancel(context.Background()) - eventDone = make(chan struct{}) - - spec, err := ebpf.LoadCollectionSpec("bpf/connection.o") - if err != nil { - return err - } - - coll, err := ebpf.NewCollection(spec) - if err != nil { - return err - } - - objs.Events = coll.Maps["events"] - - tp, err := link.Tracepoint("syscalls", "sys_enter_connect", coll.Programs["trace_connect"], nil) - if err != nil { - return err - } - - links = append(links, tp) - - // Start reading from ring buffer - reader, err = ringbuf.NewReader(objs.Events) - if err != nil { - return err - } - - // Start event processing goroutine - go processEvents() - - logger.Info("eBPF program attached to sys_enter_connect") - return nil -} - -// processEvents reads events from the ring buffer and stores them -func processEvents() { - defer close(eventDone) - logger.Info("Starting ring buffer event processing...") - - for { - select { - case <-eventCtx.Done(): - logger.Info("Event processing stopped by context cancellation") - return - default: - // Continue with event processing - } - - record, err := reader.Read() - if err != nil { - // Check if we're shutting down before logging the error - select { - case <-eventCtx.Done(): - logger.Debug("Ring buffer read error during shutdown (expected)") - return - default: - logger.Debugf("Error reading from ring buffer: %v", err) - continue - } - } - - logger.Debugf("Received ring buffer event: %d bytes", len(record.RawSample)) - logger.Debugf("Raw bytes: %x", record.RawSample) - - if len(record.RawSample) < 60 { // sizeof(event_t) packed = 4+8+4+16+4+16+2+2+1+1+2 = 60 - logger.Debugf("Event too small: %d bytes, expected at least 60", len(record.RawSample)) - continue - } - - // Expected structure layout (based on struct event_t): - // u32 pid (4 bytes) - // u64 ts (8 bytes) - // u32 ret (4 bytes) - // char comm[16] (16 bytes) - // u32 dest_ip (4 bytes) - // u8 dest_ip6[16] (16 bytes) - // u16 dest_port (2 bytes) - // u16 family (2 bytes) - // u8 protocol (1 byte) - // u8 sock_type (1 byte) - // u16 padding (2 bytes) - // Total: 60 bytes - - event := Event{ - PID: binary.LittleEndian.Uint32(record.RawSample[0:4]), - TS: binary.LittleEndian.Uint64(record.RawSample[4:12]), - Ret: int32(binary.LittleEndian.Uint32(record.RawSample[12:16])), - DestIPv4: binary.LittleEndian.Uint32(record.RawSample[32:36]), - DestPort: binary.LittleEndian.Uint16(record.RawSample[52:54]), - Family: binary.LittleEndian.Uint16(record.RawSample[54:56]), - Protocol: record.RawSample[56], - SockType: record.RawSample[57], - Padding: binary.LittleEndian.Uint16(record.RawSample[58:60]), - } - copy(event.Comm[:], record.RawSample[16:32]) - - // Handle address data based on family - family := event.Family - if family == 2 { // AF_INET - IPv4 data is at offset 32 - event.DestIPv4 = binary.LittleEndian.Uint32(record.RawSample[32:36]) - // Clear IPv6 data (should already be zero from struct initialization) - } else if family == 10 { // AF_INET6 - IPv6 data is at offset 36 - copy(event.DestIPv6[:], record.RawSample[36:52]) - // IPv4 field should remain 0 - } - - logger.Debugf("Parsed: PID=%d, TS=%d, Ret=%d, DestIP=%s, DestPort=%d, Protocol=%s", - event.PID, event.TS, event.Ret, event.GetDestIP(), event.DestPort, event.GetProtocol()) - logger.Debugf("Command bytes: %x", event.Comm[:]) - logger.Debugf("Command string: '%s'", event.GetCommand()) - - logger.Debugf("Processed event: PID=%d, Command='%s', Destination='%s', Protocol='%s', TS=%d", - event.PID, event.GetCommand(), event.GetDestination(), event.GetProtocol(), event.TS) - - // Skip events with no valid destination (common with IPv6 connections that fail address extraction) - if event.GetDestination() == "" || event.GetDestination() == ":0" { - logger.Debugf("Skipping event with invalid destination: '%s'", event.GetDestination()) - continue - } - - // Store the event - connectionsMu.Lock() - connections[event.PID] = append(connections[event.PID], event) - logger.Debugf("Stored event for PID %d, total events for this PID: %d", - event.PID, len(connections[event.PID])) - connectionsMu.Unlock() - } -} - -// GetConnectionSummary returns connection statistics for a given PID or command and duration -// Returns count of connection attempts in the specified time window -func GetConnectionSummary(pid uint32, command string, durationSeconds int) int { - connectionsMu.RLock() - defer connectionsMu.RUnlock() - - logger.Debugf("GetConnectionSummary called: pid=%d, command='%s', duration=%d", pid, command, durationSeconds) - logger.Debugf("Total PIDs in connections map: %d", len(connections)) - - // Debug: show all stored connections - for storedPid, events := range connections { - logger.Debugf(" PID %d has %d events, newest: %d", storedPid, len(events), - func() uint64 { - if len(events) > 0 { - return events[len(events)-1].TS - } - return 0 - }()) - } - - // Get current eBPF time for comparison - currentEBPFTime := time.Since(systemBootTime).Nanoseconds() - logger.Debugf("Current eBPF time (ns since boot): %d", currentEBPFTime) - - // Check if we have any events at all - totalEvents := 0 - for _, events := range connections { - totalEvents += len(events) - } - if totalEvents == 0 { - logger.Debugf("No events found, returning 0") - return 0 - } - - // Instead of using newest timestamp, use current time for cutoff calculation - // This matches the eBPF time format (nanoseconds since boot) - cutoff := uint64(currentEBPFTime) - uint64(durationSeconds)*1e9 // Convert seconds to nanoseconds - logger.Debugf("Cutoff timestamp: %d (duration: %d seconds)", cutoff, durationSeconds) - - var recentEvents []Event - - // If command is specified, search by command name across all PIDs - if command != "" { - logger.Debugf("Searching for command '%s' in the last %d seconds", command, durationSeconds) - for pid, events := range connections { - for _, event := range events { - eventCommand := event.GetCommand() - logger.Debugf("Checking PID %d: command='%s', timestamp=%d, cutoff=%d, match=%t", - pid, eventCommand, event.TS, cutoff, event.TS >= cutoff && eventCommand == command) - if event.TS >= cutoff && eventCommand == command { - recentEvents = append(recentEvents, event) - logger.Debugf(" -> Match found! Total matches so far: %d", len(recentEvents)) - } - } - } - logger.Debugf("Found %d matching events for command '%s'", len(recentEvents), command) - } else { - // Search by PID (original behavior) - events, exists := connections[pid] - if !exists { - logger.Debugf("PID %d not found in connections", pid) - return 0 - } - - logger.Debugf("Searching PID %d events (total: %d)", pid, len(events)) - for _, event := range events { - logger.Debugf(" Event TS: %d, cutoff: %d, include: %t", event.TS, cutoff, event.TS >= cutoff) - if event.TS >= cutoff { - recentEvents = append(recentEvents, event) - } - } - logger.Debugf("Found %d recent events for PID %d", len(recentEvents), pid) - } - - if len(recentEvents) == 0 { - logger.Debugf("No recent events found, returning 0") - return 0 - } - - logger.Debugf("Returning count: %d", len(recentEvents)) - // Return the count of connection attempts in the time window - return len(recentEvents) -} - -// GetAllConnections returns all tracked connections (for debugging) -func GetAllConnections() map[uint32][]Event { - connectionsMu.RLock() - defer connectionsMu.RUnlock() - - result := make(map[uint32][]Event) - for pid, events := range connections { - result[pid] = make([]Event, len(events)) - copy(result[pid], events) - } - return result -} - -// Cleanup closes the ring buffer reader and detaches programs -func Cleanup() { - logger.Info("Starting eBPF cleanup...") - - // Signal the event processing goroutine to stop - if eventCancel != nil { - logger.Debug("Cancelling event processing context...") - eventCancel() - } - - // Wait for the event processing goroutine to finish (with timeout) - if eventDone != nil { - logger.Debug("Waiting for event processing to complete...") - select { - case <-eventDone: - logger.Debug("Event processing goroutine finished") - case <-time.After(5 * time.Second): - logger.Error("Timeout waiting for event processing to finish") - } - } - - // Close ring buffer reader - if reader != nil { - logger.Debug("Closing ring buffer reader...") - reader.Close() - reader = nil - } - - // Close links - logger.Debug("Closing eBPF links...") - for _, l := range links { - l.Close() - } - links = nil - - // Close maps - if objs.Events != nil { - logger.Debug("Closing eBPF maps...") - objs.Events.Close() - objs.Events = nil - } - - logger.Info("eBPF cleanup complete") -} diff --git a/internal/bpf/types.go b/internal/bpf/types.go deleted file mode 100644 index cacd714..0000000 --- a/internal/bpf/types.go +++ /dev/null @@ -1,160 +0,0 @@ -package bpf - -import ( - "encoding/json" - "fmt" - "net" - "time" - "unsafe" -) - -type Event struct { - PID uint32 `json:"pid"` - TS uint64 `json:"timestamp_ns"` - Ret int32 `json:"return_code"` - Comm [16]byte `json:"-"` - DestIPv4 uint32 `json:"dest_ip"` // IPv4 address (0 if IPv6) - DestIPv6 [16]byte `json:"dest_ip6"` // IPv6 address (all zeros if IPv4) - DestPort uint16 `json:"dest_port"` - Family uint16 `json:"address_family"` - Protocol uint8 `json:"protocol"` - SockType uint8 `json:"socket_type"` - Padding uint16 `json:"-"` -} - -// GetCommand returns the command name as a string -func (e *Event) GetCommand() string { - return string((*(*[16]byte)(unsafe.Pointer(&e.Comm[0])))[:clen(e.Comm[:])]) -} - -// GetDestIP returns the destination IP as a string -func (e *Event) GetDestIP() string { - const AF_INET = 2 - const AF_INET6 = 10 - - switch e.Family { - case AF_INET: - if e.DestIPv4 == 0 { - return "" - } - // Convert from little-endian uint32 to IP address - ip := net.IPv4(byte(e.DestIPv4), byte(e.DestIPv4>>8), byte(e.DestIPv4>>16), byte(e.DestIPv4>>24)) - return ip.String() - case AF_INET6: - // Check if IPv6 address is all zeros - allZero := true - for _, b := range e.DestIPv6 { - if b != 0 { - allZero = false - break - } - } - if allZero { - return "" - } - ip := net.IP(e.DestIPv6[:]) - return ip.String() - default: - return "" - } -} - -// GetDestination returns the destination as "IP:port" format -func (e *Event) GetDestination() string { - const AF_INET6 = 10 - - ip := e.GetDestIP() - if ip == "" { - return "" - } - - // IPv6 addresses need to be wrapped in brackets - if e.Family == AF_INET6 { - return fmt.Sprintf("[%s]:%d", ip, e.DestPort) - } - - return fmt.Sprintf("%s:%d", ip, e.DestPort) -} - -// GetProtocol returns the protocol as a string -func (e *Event) GetProtocol() string { - switch e.Protocol { - case 6: // IPPROTO_TCP - return "TCP" - case 17: // IPPROTO_UDP - return "UDP" - default: - return "Unknown" - } -} - -// GetSocketType returns the socket type as a string -func (e *Event) GetSocketType() string { - switch e.SockType { - case 1: // SOCK_STREAM - return "STREAM" - case 2: // SOCK_DGRAM - return "DGRAM" - default: - return "Unknown" - } -} - -// GetTime returns a placeholder since eBPF timestamps are boot-relative, not wall-clock time -func (e *Event) GetTime() time.Time { - // eBPF uses ktime_get_ns() which is nanoseconds since boot, not Unix epoch - // Converting to wall-clock time requires additional boot time calculation - // For now, return a zero time to avoid misleading 1970 dates - return time.Time{} -} - -// GetWallClockTime converts eBPF timestamp to wall clock time using system boot time -func (e *Event) GetWallClockTime() time.Time { - // Convert eBPF timestamp (nanoseconds since boot) to wall clock time - // This requires access to the systemBootTime from loader.go - return GetSystemBootTime().Add(time.Duration(e.TS) * time.Nanosecond) -} - -// EventJSON is used for JSON serialization with human-readable fields -type EventJSON struct { - PID uint32 `json:"pid"` - Timestamp uint64 `json:"timestamp_ns"` - RetCode int32 `json:"return_code"` - Command string `json:"command"` - DestIP string `json:"destination_ip"` - DestPort uint16 `json:"destination_port"` - Destination string `json:"destination"` - Family uint16 `json:"address_family"` - Protocol string `json:"protocol"` - SocketType string `json:"socket_type"` - WallTime string `json:"wall_time"` - Note string `json:"note"` -} - -// MarshalJSON implements custom JSON marshaling -func (e *Event) MarshalJSON() ([]byte, error) { - return json.Marshal(EventJSON{ - PID: e.PID, - Timestamp: e.TS, - RetCode: e.Ret, - Command: e.GetCommand(), - DestIP: e.GetDestIP(), - DestPort: e.DestPort, - Destination: e.GetDestination(), - Family: e.Family, - Protocol: e.GetProtocol(), - SocketType: e.GetSocketType(), - WallTime: e.GetWallClockTime().Format(time.RFC3339), - Note: "timestamp_ns is nanoseconds since boot, wall_time is converted to UTC", - }) -} - -// clen finds the length of a null-terminated C string -func clen(b []byte) int { - for i := 0; i < len(b); i++ { - if b[i] == 0 { - return i - } - } - return len(b) -} diff --git a/internal/bpf/types_test.go b/internal/bpf/types_test.go deleted file mode 100644 index 4cbd2fa..0000000 --- a/internal/bpf/types_test.go +++ /dev/null @@ -1,185 +0,0 @@ -package bpf - -import ( - "testing" - "time" -) - -func TestEventGetters(t *testing.T) { - // Create a test event for IPv4 - event := Event{ - PID: 1234, - TS: 1000000000000, // 1 second since boot in nanoseconds - Ret: 0, - Comm: [16]byte{'t', 'e', 's', 't', 0}, // null-terminated string - DestIPv4: 0x08080808, // 8.8.8.8 in network byte order (little endian) - DestPort: 53, - Family: 2, // AF_INET - Protocol: 17, // UDP - SockType: 2, // SOCK_DGRAM - } - - // Test GetCommand - if cmd := event.GetCommand(); cmd != "test" { - t.Errorf("GetCommand() = %q, want %q", cmd, "test") - } - - // Test GetDestIP - if ip := event.GetDestIP(); ip != "8.8.8.8" { - t.Errorf("GetDestIP() = %q, want %q", ip, "8.8.8.8") - } - - // Test GetDestination - if dest := event.GetDestination(); dest != "8.8.8.8:53" { - t.Errorf("GetDestination() = %q, want %q", dest, "8.8.8.8:53") - } - - // Test GetProtocol - if proto := event.GetProtocol(); proto != "UDP" { - t.Errorf("GetProtocol() = %q, want %q", proto, "UDP") - } - - // Test GetSocketType - if sockType := event.GetSocketType(); sockType != "DGRAM" { - t.Errorf("GetSocketType() = %q, want %q", sockType, "DGRAM") - } -} - -func TestEventGettersUnknown(t *testing.T) { - // Test unknown values - event := Event{ - Protocol: 255, // Unknown protocol - SockType: 255, // Unknown socket type - DestIPv4: 0, // Invalid IP - Family: 2, // AF_INET - } - - if proto := event.GetProtocol(); proto != "Unknown" { - t.Errorf("GetProtocol() = %q, want %q", proto, "Unknown") - } - - if sockType := event.GetSocketType(); sockType != "Unknown" { - t.Errorf("GetSocketType() = %q, want %q", sockType, "Unknown") - } - - if ip := event.GetDestIP(); ip != "" { - t.Errorf("GetDestIP() = %q, want empty string for zero IP", ip) - } - - if dest := event.GetDestination(); dest != "" { - t.Errorf("GetDestination() = %q, want empty string for zero IP", dest) - } -} - -func TestEventTimeConversion(t *testing.T) { - // Set a mock boot time for testing - originalBootTime := systemBootTime - systemBootTime = time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC) - defer func() { - systemBootTime = originalBootTime - }() - - event := Event{ - TS: 5000000000, // 5 seconds since boot in nanoseconds - } - - wallTime := event.GetWallClockTime() // Use GetWallClockTime instead of GetTime - expectedTime := systemBootTime.Add(5 * time.Second) - - if !wallTime.Equal(expectedTime) { - t.Errorf("GetWallClockTime() = %v, want %v", wallTime, expectedTime) - } - - // Test that GetTime returns zero time (as documented) - zeroTime := event.GetTime() - if !zeroTime.IsZero() { - t.Errorf("GetTime() should return zero time, got %v", zeroTime) - } -} - -func TestTcpProtocolDetection(t *testing.T) { - tests := []struct { - protocol uint8 - sockType uint8 - expected string - }{ - {6, 1, "TCP"}, // IPPROTO_TCP with SOCK_STREAM - {17, 2, "UDP"}, // IPPROTO_UDP with SOCK_DGRAM - {1, 1, "Unknown"}, // IPPROTO_ICMP - {255, 1, "Unknown"}, // Unknown protocol - } - - for _, tt := range tests { - event := Event{ - Protocol: tt.protocol, - SockType: tt.sockType, - } - - if proto := event.GetProtocol(); proto != tt.expected { - t.Errorf("Protocol %d: GetProtocol() = %q, want %q", tt.protocol, proto, tt.expected) - } - } -} - -func TestSocketTypeDetection(t *testing.T) { - tests := []struct { - sockType uint8 - expected string - }{ - {1, "STREAM"}, // SOCK_STREAM - {2, "DGRAM"}, // SOCK_DGRAM - {3, "Unknown"}, // SOCK_RAW - {255, "Unknown"}, // Unknown socket type - } - - for _, tt := range tests { - event := Event{ - SockType: tt.sockType, - } - - if sockType := event.GetSocketType(); sockType != tt.expected { - t.Errorf("SockType %d: GetSocketType() = %q, want %q", tt.sockType, sockType, tt.expected) - } - } -} - -func TestIPv6Support(t *testing.T) { - // Test IPv6 address parsing - ipv6Bytes := [16]byte{ - 0x26, 0x00, 0x14, 0x06, 0xbc, 0x00, 0x00, 0x53, - 0x00, 0x00, 0x00, 0x00, 0xb8, 0x1e, 0x94, 0xc8, - } // 2600:1406:bc00:53::b81e:94c8 - - event := Event{ - PID: 1234, - TS: 1000000000000, - Ret: 0, - Comm: [16]byte{'c', 'u', 'r', 'l', 0}, - DestIPv6: ipv6Bytes, - DestPort: 80, - Family: 10, // AF_INET6 - Protocol: 6, // TCP - SockType: 1, // SOCK_STREAM - } - - // Test GetDestIP for IPv6 - expectedIP := "2600:1406:bc00:53::b81e:94c8" - if ip := event.GetDestIP(); ip != expectedIP { - t.Errorf("GetDestIP() for IPv6 = %q, want %q", ip, expectedIP) - } - - // Test GetDestination for IPv6 - expectedDest := "[2600:1406:bc00:53::b81e:94c8]:80" - if dest := event.GetDestination(); dest != expectedDest { - t.Errorf("GetDestination() for IPv6 = %q, want %q", dest, expectedDest) - } - - // Test with empty IPv6 address - emptyEvent := Event{ - Family: 10, // AF_INET6 - // DestIPv6 is all zeros by default - } - if ip := emptyEvent.GetDestIP(); ip != "" { - t.Errorf("GetDestIP() for empty IPv6 = %q, want empty string", ip) - } -} diff --git a/internal/core/types.go b/internal/core/types.go new file mode 100644 index 0000000..b4e5223 --- /dev/null +++ b/internal/core/types.go @@ -0,0 +1,156 @@ +// Package core defines the fundamental types and interfaces for the eBPF monitoring system. +// It provides the contracts that all other components must implement. +package core + +import ( + "context" + "encoding/json" + "time" +) + +// Event represents a single eBPF event with metadata. +// All events in the system implement this interface. +type Event interface { + // ID returns a unique identifier for this event + ID() string + + // Type returns the event type (e.g., "connection", "packet_drop") + Type() string + + // PID returns the process ID that generated this event + PID() uint32 + + // Command returns the command name of the process + Command() string + + // Timestamp returns the kernel timestamp (nanoseconds since boot) + Timestamp() uint64 + + // Time returns the wall clock time when the event occurred + Time() time.Time + + // Metadata returns event-specific data as a map + Metadata() map[string]interface{} + + // JSON serialization + json.Marshaler +} + +// EventParser converts raw binary data from eBPF programs into Event objects. +type EventParser interface { + // Parse converts raw bytes into an Event + Parse(data []byte) (Event, error) + + // EventType returns the type of events this parser handles + EventType() string +} + +// EventStream provides a channel-based interface for receiving events. +type EventStream interface { + // Events returns a channel that delivers events + Events() <-chan Event + + // Close stops the event stream and closes the channel + Close() error +} + +// EventSink stores events for later retrieval. +type EventSink interface { + // Store saves an event + Store(ctx context.Context, event Event) error + + // Query retrieves events matching the given criteria + Query(ctx context.Context, query Query) ([]Event, error) + + // Count returns the number of events matching the criteria + Count(ctx context.Context, query Query) (int, error) +} + +// Program represents an eBPF program that can be loaded and attached to the kernel. +type Program interface { + // Name returns the program name + Name() string + + // Description returns a human-readable description + Description() string + + // Load compiles and loads the eBPF program into the kernel + Load(ctx context.Context) error + + // Attach attaches the program to appropriate kernel hooks + Attach(ctx context.Context) error + + // Detach detaches the program from kernel hooks + Detach(ctx context.Context) error + + // IsLoaded returns true if the program is loaded + IsLoaded() bool + + // IsAttached returns true if the program is attached + IsAttached() bool + + // EventStream returns a stream of events from this program + EventStream() EventStream + + // GetStats returns event processing statistics + GetStats() (totalEvents, droppedEvents uint64, dropRate float64) +} + +// Manager orchestrates multiple eBPF programs and provides a unified interface. +type Manager interface { + // RegisterProgram adds a program to the manager + RegisterProgram(program Program) error + + // LoadAll loads all registered programs + LoadAll(ctx context.Context) error + + // AttachAll attaches all loaded programs + AttachAll(ctx context.Context) error + + // DetachAll detaches all programs + DetachAll(ctx context.Context) error + + // Programs returns all registered programs + Programs() []Program + + // GetProgramStatus returns status of all programs + GetProgramStatus() []ProgramStatus + + // EventStream returns a unified stream of events from all programs + EventStream() EventStream + + // IsRunning returns true if the manager is active + IsRunning() bool +} + +// Query represents search criteria for events. +type Query struct { + // EventType filters by event type (optional) + EventType string + + // PID filters by process ID (optional, 0 means no filter) + PID uint32 + + // Command filters by command name (optional) + Command string + + // Since filters events after this time (optional) + Since time.Time + + // Until filters events before this time (optional) + Until time.Time + + // Limit limits the number of results (optional, 0 means no limit) + Limit int +} + +// ProgramStatus represents the current state of a program. +type ProgramStatus struct { + Name string `json:"name"` + Description string `json:"description"` + Loaded bool `json:"loaded"` + Attached bool `json:"attached"` + EventCount int64 `json:"event_count"` + DroppedCount int64 `json:"dropped_count"` + DropRate float64 `json:"drop_rate"` +} diff --git a/internal/core/types_test.go b/internal/core/types_test.go new file mode 100644 index 0000000..66636a1 --- /dev/null +++ b/internal/core/types_test.go @@ -0,0 +1,465 @@ +package core + +import ( + "context" + "encoding/json" + "testing" + "time" +) + +// MockEvent implements the Event interface for testing +type MockEvent struct { + id string + eventType string + pid uint32 + command string + timestamp uint64 + time time.Time + metadata map[string]interface{} +} + +func (m *MockEvent) ID() string { return m.id } +func (m *MockEvent) Type() string { return m.eventType } +func (m *MockEvent) PID() uint32 { return m.pid } +func (m *MockEvent) Command() string { return m.command } +func (m *MockEvent) Timestamp() uint64 { return m.timestamp } +func (m *MockEvent) Time() time.Time { return m.time } +func (m *MockEvent) Metadata() map[string]interface{} { return m.metadata } + +func (m *MockEvent) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]interface{}{ + "id": m.id, + "type": m.eventType, + "pid": m.pid, + "command": m.command, + "timestamp": m.timestamp, + "time": m.time.Format(time.RFC3339), + "metadata": m.metadata, + }) +} + +// MockEventParser implements EventParser for testing +type MockEventParser struct { + eventType string + parseFunc func([]byte) (Event, error) +} + +func (m *MockEventParser) Parse(data []byte) (Event, error) { + if m.parseFunc != nil { + return m.parseFunc(data) + } + return &MockEvent{ + id: "test-id", + eventType: m.eventType, + pid: 1234, + command: "test-cmd", + timestamp: uint64(time.Now().UnixNano()), + time: time.Now(), + metadata: map[string]interface{}{"test": "data"}, + }, nil +} + +func (m *MockEventParser) EventType() string { + return m.eventType +} + +// MockEventStream implements EventStream for testing +type MockEventStream struct { + events chan Event + closed bool +} + +func (m *MockEventStream) Events() <-chan Event { return m.events } +func (m *MockEventStream) Close() error { + if !m.closed { + close(m.events) + m.closed = true + } + return nil +} + +// MockEventSink implements EventSink for testing +type MockEventSink struct { + events []Event +} + +func (m *MockEventSink) Consume(stream EventStream) error { + for event := range stream.Events() { + m.events = append(m.events, event) + } + return nil +} + +func (m *MockEventSink) GetEvents() []Event { + return m.events +} + +// MockProgram implements Program for testing +type MockProgram struct { + name string + description string + loaded bool + attached bool + stream EventStream +} + +func (m *MockProgram) Name() string { return m.name } +func (m *MockProgram) Description() string { return m.description } +func (m *MockProgram) Load(ctx context.Context) error { m.loaded = true; return nil } +func (m *MockProgram) Attach(ctx context.Context) error { m.attached = true; return nil } +func (m *MockProgram) Detach(ctx context.Context) error { m.attached = false; return nil } +func (m *MockProgram) IsLoaded() bool { return m.loaded } +func (m *MockProgram) IsAttached() bool { return m.attached } +func (m *MockProgram) EventStream() EventStream { return m.stream } +func (m *MockProgram) GetStats() (uint64, uint64, float64) { return 0, 0, 0.0 } + +// MockManager implements Manager for testing +type MockManager struct { + programs []Program + running bool +} + +func (m *MockManager) RegisterProgram(program Program) error { m.programs = append(m.programs, program); return nil } +func (m *MockManager) LoadAll(ctx context.Context) error { m.running = true; return nil } +func (m *MockManager) AttachAll(ctx context.Context) error { return nil } +func (m *MockManager) DetachAll(ctx context.Context) error { return nil } +func (m *MockManager) Programs() []Program { return m.programs } +func (m *MockManager) GetProgramStatus() []ProgramStatus { return []ProgramStatus{} } +func (m *MockManager) EventStream() EventStream { return &MockEventStream{events: make(chan Event)} } +func (m *MockManager) IsRunning() bool { return m.running } + +// TestEvent tests the Event interface +func TestEvent(t *testing.T) { + now := time.Now() + timestamp := uint64(now.UnixNano()) + + event := &MockEvent{ + id: "test-123", + eventType: "connection", + pid: 1234, + command: "curl", + timestamp: timestamp, + time: now, + metadata: map[string]interface{}{"dest": "127.0.0.1:80"}, + } + + if event.ID() != "test-123" { + t.Errorf("expected ID 'test-123', got %s", event.ID()) + } + + if event.Type() != "connection" { + t.Errorf("expected type 'connection', got %s", event.Type()) + } + + if event.PID() != 1234 { + t.Errorf("expected PID 1234, got %d", event.PID()) + } + + if event.Command() != "curl" { + t.Errorf("expected command 'curl', got %s", event.Command()) + } + + if event.Timestamp() != timestamp { + t.Errorf("expected timestamp %d, got %d", timestamp, event.Timestamp()) + } + + if !event.Time().Equal(now) { + t.Errorf("expected time %v, got %v", now, event.Time()) + } + + metadata := event.Metadata() + if dest, ok := metadata["dest"]; !ok || dest != "127.0.0.1:80" { + t.Errorf("expected metadata dest '127.0.0.1:80', got %v", dest) + } + + // Test JSON marshaling + jsonData, err := json.Marshal(event) + if err != nil { + t.Fatalf("failed to marshal event to JSON: %v", err) + } + + var unmarshaled map[string]interface{} + if err := json.Unmarshal(jsonData, &unmarshaled); err != nil { + t.Fatalf("failed to unmarshal event JSON: %v", err) + } + + if unmarshaled["id"] != "test-123" { + t.Errorf("expected JSON id 'test-123', got %v", unmarshaled["id"]) + } +} + +// TestEventParser tests the EventParser interface +func TestEventParser(t *testing.T) { + parser := &MockEventParser{ + eventType: "test", + } + + if parser.EventType() != "test" { + t.Errorf("expected event type 'test', got %s", parser.EventType()) + } + + event, err := parser.Parse([]byte("test data")) + if err != nil { + t.Fatalf("unexpected error parsing data: %v", err) + } + + if event.Type() != "test" { + t.Errorf("expected parsed event type 'test', got %s", event.Type()) + } +} + +// TestEventStream tests the EventStream interface +func TestEventStream(t *testing.T) { + stream := &MockEventStream{ + events: make(chan Event, 2), + } + + // Send some events + event1 := &MockEvent{id: "1", eventType: "test"} + event2 := &MockEvent{id: "2", eventType: "test"} + + stream.events <- event1 + stream.events <- event2 + + // Read events with proper synchronization + events := make([]Event, 0, 2) + done := make(chan bool) + go func() { + defer close(done) + for event := range stream.Events() { + events = append(events, event) + } + }() + + // Close stream + if err := stream.Close(); err != nil { + t.Fatalf("unexpected error closing stream: %v", err) + } + + // Wait for goroutine to finish + <-done + + if len(events) != 2 { + t.Errorf("expected 2 events, got %d", len(events)) + } + + if events[0].ID() != "1" { + t.Errorf("expected first event ID '1', got %s", events[0].ID()) + } + + if events[1].ID() != "2" { + t.Errorf("expected second event ID '2', got %s", events[1].ID()) + } +} + +// TestEventSink tests the EventSink interface +func TestEventSink(t *testing.T) { + sink := &MockEventSink{ + events: make([]Event, 0), + } + + stream := &MockEventStream{ + events: make(chan Event, 2), + } + + // Send events to stream + event1 := &MockEvent{id: "1", eventType: "test"} + event2 := &MockEvent{id: "2", eventType: "test"} + + go func() { + stream.events <- event1 + stream.events <- event2 + stream.Close() + }() + + // Consume events + if err := sink.Consume(stream); err != nil { + t.Fatalf("unexpected error consuming events: %v", err) + } + + events := sink.GetEvents() + if len(events) != 2 { + t.Errorf("expected 2 consumed events, got %d", len(events)) + } +} + +// TestProgram tests the Program interface +func TestProgram(t *testing.T) { + stream := &MockEventStream{ + events: make(chan Event), + } + + program := &MockProgram{ + name: "test-program", + description: "Test program for connections", + stream: stream, + } + + if program.Name() != "test-program" { + t.Errorf("expected name 'test-program', got %s", program.Name()) + } + + if program.Description() != "Test program for connections" { + t.Errorf("expected description 'Test program for connections', got %s", program.Description()) + } + + if program.IsLoaded() { + t.Error("program should not be loaded initially") + } + + if program.IsAttached() { + t.Error("program should not be attached initially") + } + + ctx := context.Background() + + // Test loading + if err := program.Load(ctx); err != nil { + t.Fatalf("unexpected error loading program: %v", err) + } + + if !program.IsLoaded() { + t.Error("program should be loaded after Load()") + } + + // Test attaching + if err := program.Attach(ctx); err != nil { + t.Fatalf("unexpected error attaching program: %v", err) + } + + if !program.IsAttached() { + t.Error("program should be attached after Attach()") + } + + // Test event stream + if program.EventStream() != stream { + t.Error("program should return the correct event stream") + } + + // Test detaching + if err := program.Detach(ctx); err != nil { + t.Fatalf("unexpected error detaching program: %v", err) + } + + if program.IsAttached() { + t.Error("program should not be attached after Detach()") + } +} + +// TestManager tests the Manager interface +func TestManager(t *testing.T) { + manager := &MockManager{ + programs: make([]Program, 0), + } + + if manager.IsRunning() { + t.Error("manager should not be running initially") + } + + // Test adding programs + program1 := &MockProgram{name: "prog1", description: "Connection program"} + program2 := &MockProgram{name: "prog2", description: "Packet drop program"} + + if err := manager.RegisterProgram(program1); err != nil { + t.Fatalf("unexpected error registering program1: %v", err) + } + + if err := manager.RegisterProgram(program2); err != nil { + t.Fatalf("unexpected error registering program2: %v", err) + } + + programs := manager.Programs() + if len(programs) != 2 { + t.Errorf("expected 2 programs, got %d", len(programs)) + } + + // Test loading all + ctx := context.Background() + if err := manager.LoadAll(ctx); err != nil { + t.Fatalf("unexpected error loading all programs: %v", err) + } + + if !manager.IsRunning() { + t.Error("manager should be running after LoadAll()") + } + + // Test attaching all + if err := manager.AttachAll(ctx); err != nil { + t.Fatalf("unexpected error attaching all programs: %v", err) + } + + // Test detaching all + if err := manager.DetachAll(ctx); err != nil { + t.Fatalf("unexpected error detaching all programs: %v", err) + } + + // Test getting program status + status := manager.GetProgramStatus() + if status == nil { + t.Error("expected non-nil program status") + } + + // Test event stream + stream := manager.EventStream() + if stream == nil { + t.Error("expected non-nil event stream") + } +} + +// TestQuery tests the Query struct +func TestQuery(t *testing.T) { + now := time.Now() + query := Query{ + EventType: "connection", + PID: 1234, + Command: "curl", + Since: now.Add(-1 * time.Hour), + Until: now, + Limit: 100, + } + + if query.EventType != "connection" { + t.Errorf("expected event type 'connection', got %s", query.EventType) + } + + if query.PID != 1234 { + t.Errorf("expected PID 1234, got %d", query.PID) + } + + if query.Command != "curl" { + t.Errorf("expected command 'curl', got %s", query.Command) + } + + if query.Limit != 100 { + t.Errorf("expected limit 100, got %d", query.Limit) + } + + if query.Since.After(now.Add(-1*time.Hour)) || query.Since.Before(now.Add(-2*time.Hour)) { + t.Errorf("expected since time around 1 hour ago, got %v", query.Since) + } + + if query.Until.After(now.Add(time.Minute)) || query.Until.Before(now.Add(-time.Minute)) { + t.Errorf("expected until time around now, got %v", query.Until) + } +} + +// TestDefaultQuery tests query with default values +func TestDefaultQuery(t *testing.T) { + query := Query{} + + if query.EventType != "" { + t.Errorf("expected empty event type, got %s", query.EventType) + } + + if query.PID != 0 { + t.Errorf("expected PID 0, got %d", query.PID) + } + + if query.Command != "" { + t.Errorf("expected empty command, got %s", query.Command) + } + + if query.Limit != 0 { + t.Errorf("expected limit 0, got %d", query.Limit) + } +} diff --git a/internal/events/events.go b/internal/events/events.go new file mode 100644 index 0000000..02882b9 --- /dev/null +++ b/internal/events/events.go @@ -0,0 +1,322 @@ +// Package events provides event implementations and utilities for the eBPF monitoring system. +package events + +import ( + "context" + "crypto/rand" + "encoding/hex" + "encoding/json" + "fmt" + "os" + "strconv" + "strings" + "sync" + "time" + + "github.com/srodi/ebpf-server/internal/core" + "github.com/srodi/ebpf-server/pkg/logger" +) + +var ( + // Cached boot time to avoid recalculating it for every event + systemBootTime time.Time + bootTimeCalculated bool + bootTimeMutex sync.Mutex +) + +// calculateSystemBootTime calculates the system boot time. +// On Linux, it reads /proc/stat to get the 'btime' field. +// On other platforms, it uses a fallback method. +func calculateSystemBootTime() time.Time { + bootTimeMutex.Lock() + defer bootTimeMutex.Unlock() + + if bootTimeCalculated { + return systemBootTime + } + + // Try Linux-specific method first + if bootTime, err := getBootTimeLinux(); err == nil { + systemBootTime = bootTime + bootTimeCalculated = true + logger.Debugf("System boot time calculated (Linux): %v", systemBootTime) + return systemBootTime + } + + // Fallback for non-Linux systems or when /proc/stat is unavailable + // This provides a reasonable approximation for development/testing + systemBootTime = time.Now().Add(-time.Hour * 24) // Assume system has been up for less than 24 hours + bootTimeCalculated = true + logger.Debugf("System boot time calculated (fallback): %v", systemBootTime) + return systemBootTime +} + +// getBootTimeLinux reads boot time from /proc/stat (Linux-specific). +func getBootTimeLinux() (time.Time, error) { + data, err := os.ReadFile("/proc/stat") + if err != nil { + return time.Time{}, err + } + + // Parse /proc/stat to find the btime line + lines := strings.Split(string(data), "\n") + for _, line := range lines { + if strings.HasPrefix(line, "btime ") { + fields := strings.Fields(line) + if len(fields) >= 2 { + bootTimeSeconds, err := strconv.ParseInt(fields[1], 10, 64) + if err != nil { + return time.Time{}, err + } + return time.Unix(bootTimeSeconds, 0), nil + } + } + } + + return time.Time{}, fmt.Errorf("btime not found in /proc/stat") +} + +// convertEBPFTimestamp converts an eBPF timestamp (nanoseconds since boot) to wall-clock time. +// eBPF timestamps are typically obtained using bpf_ktime_get_ns() which returns nanoseconds +// since system boot. To convert to wall-clock time, we add this to the system boot time. +func convertEBPFTimestamp(ebpfTimestampNs uint64) time.Time { + bootTime := calculateSystemBootTime() + + // Add the eBPF timestamp (nanoseconds since boot) to the boot time + return bootTime.Add(time.Duration(ebpfTimestampNs) * time.Nanosecond) +} + +// ResetBootTimeCache resets the cached boot time calculation. +// This is useful for testing or if the system time changes significantly. +func ResetBootTimeCache() { + bootTimeMutex.Lock() + defer bootTimeMutex.Unlock() + bootTimeCalculated = false + systemBootTime = time.Time{} +} + +// BaseEvent provides common functionality for all eBPF events. +type BaseEvent struct { + id string + eventType string + pid uint32 + command string + timestamp uint64 + time time.Time + metadata map[string]interface{} +} + +// NewBaseEvent creates a new base event. +func NewBaseEvent(eventType string, pid uint32, command string, timestamp uint64, metadata map[string]interface{}) *BaseEvent { + // Generate a unique ID + idBytes := make([]byte, 8) + if _, err := rand.Read(idBytes); err != nil { + // If crypto/rand fails, use a simple fallback + for i := range idBytes { + idBytes[i] = byte(i) + } + } + id := hex.EncodeToString(idBytes) + + // Convert eBPF timestamp to wall clock time using proper boot time calculation + // eBPF timestamps are nanoseconds since boot (from bpf_ktime_get_ns()) + eventTime := convertEBPFTimestamp(timestamp) + + return &BaseEvent{ + id: id, + eventType: eventType, + pid: pid, + command: command, + timestamp: timestamp, + time: eventTime, + metadata: metadata, + } +} + +// ID returns the unique event identifier. +func (e *BaseEvent) ID() string { + return e.id +} + +// Type returns the event type. +func (e *BaseEvent) Type() string { + return e.eventType +} + +// PID returns the process ID. +func (e *BaseEvent) PID() uint32 { + return e.pid +} + +// Command returns the command name. +func (e *BaseEvent) Command() string { + return e.command +} + +// Timestamp returns the kernel timestamp. +func (e *BaseEvent) Timestamp() uint64 { + return e.timestamp +} + +// Time returns the wall clock time. +func (e *BaseEvent) Time() time.Time { + return e.time +} + +// Metadata returns the event metadata. +func (e *BaseEvent) Metadata() map[string]interface{} { + return e.metadata +} + +// MarshalJSON implements json.Marshaler. +func (e *BaseEvent) MarshalJSON() ([]byte, error) { + data := map[string]interface{}{ + "id": e.id, + "type": e.eventType, + "pid": e.pid, + "command": e.command, + "timestamp": e.timestamp, + "time": e.time.Format(time.RFC3339Nano), + } + + // Add metadata fields + for k, v := range e.metadata { + data[k] = v + } + + return json.Marshal(data) +} + +// ChannelStream implements EventStream using a channel. +type ChannelStream struct { + events chan core.Event + closed bool + mu sync.RWMutex +} + +// NewChannelStream creates a new channel-based event stream. +func NewChannelStream(bufferSize int) *ChannelStream { + return &ChannelStream{ + events: make(chan core.Event, bufferSize), + } +} + +// Events returns the event channel. +func (s *ChannelStream) Events() <-chan core.Event { + return s.events +} + +// Send adds an event to the stream (non-blocking). +func (s *ChannelStream) Send(event core.Event) bool { + s.mu.RLock() + defer s.mu.RUnlock() + + if s.closed { + return false + } + + select { + case s.events <- event: + return true + default: + // Channel is full, drop the event + return false + } +} + +// Close stops the event stream. +func (s *ChannelStream) Close() error { + s.mu.Lock() + defer s.mu.Unlock() + + if !s.closed { + s.closed = true + close(s.events) + } + + return nil +} + +// MergedStream merges events from multiple EventStreams. +type MergedStream struct { + streams []core.EventStream + events chan core.Event + ctx context.Context + cancel context.CancelFunc + wg sync.WaitGroup + closed bool + mu sync.Mutex +} + +// NewMergedStream creates a stream that merges events from multiple sources. +func NewMergedStream(streams []core.EventStream) *MergedStream { + ctx, cancel := context.WithCancel(context.Background()) + merged := &MergedStream{ + streams: streams, + events: make(chan core.Event, 1000), + ctx: ctx, + cancel: cancel, + } + + // Start goroutines to read from each stream + for _, stream := range streams { + merged.wg.Add(1) + go merged.readFromStream(stream) + } + + return merged +} + +// Events returns the merged event channel. +func (m *MergedStream) Events() <-chan core.Event { + return m.events +} + +// Close stops the merged stream. +func (m *MergedStream) Close() error { + m.mu.Lock() + if m.closed { + m.mu.Unlock() + return nil + } + m.closed = true + m.mu.Unlock() + + // Cancel context to signal all goroutines to stop + m.cancel() + + // Close all source streams + for _, stream := range m.streams { + stream.Close() + } + + // Wait for all goroutines to finish before closing the events channel + m.wg.Wait() + close(m.events) + + return nil +} + +// readFromStream reads events from a source stream and forwards them. +func (m *MergedStream) readFromStream(stream core.EventStream) { + defer m.wg.Done() + + for { + select { + case event, ok := <-stream.Events(): + if !ok { + return + } + + // Try to send the event, but respect context cancellation + select { + case m.events <- event: + case <-m.ctx.Done(): + return + } + + case <-m.ctx.Done(): + return + } + } +} diff --git a/internal/events/events_test.go b/internal/events/events_test.go new file mode 100644 index 0000000..b7adcd0 --- /dev/null +++ b/internal/events/events_test.go @@ -0,0 +1,428 @@ +package events + +import ( + "encoding/json" + "testing" + "time" + + "github.com/srodi/ebpf-server/internal/core" +) + +// TestBaseEvent tests the BaseEvent implementation +func TestBaseEvent(t *testing.T) { + metadata := map[string]interface{}{ + "dest": "127.0.0.1:80", + "proto": "tcp", + } + + event := NewBaseEvent("connection", 1234, "curl", 1000000, metadata) + + // Test that ID is generated + if event.ID() == "" { + t.Error("expected non-empty ID") + } + + // Test type + if event.Type() != "connection" { + t.Errorf("expected type 'connection', got %s", event.Type()) + } + + // Test PID + if event.PID() != 1234 { + t.Errorf("expected PID 1234, got %d", event.PID()) + } + + // Test command + if event.Command() != "curl" { + t.Errorf("expected command 'curl', got %s", event.Command()) + } + + // Test timestamp + if event.Timestamp() != 1000000 { + t.Errorf("expected timestamp 1000000, got %d", event.Timestamp()) + } + + // Test time is set + if event.Time().IsZero() { + t.Error("expected non-zero time") + } + + // Test metadata + eventMetadata := event.Metadata() + if dest, ok := eventMetadata["dest"]; !ok || dest != "127.0.0.1:80" { + t.Errorf("expected metadata dest '127.0.0.1:80', got %v", dest) + } + + // Test JSON marshaling + jsonData, err := event.MarshalJSON() + if err != nil { + t.Fatalf("failed to marshal event to JSON: %v", err) + } + + if len(jsonData) == 0 { + t.Error("expected non-empty JSON data") + } + + // Verify JSON contains expected fields + var result map[string]interface{} + if err := json.Unmarshal(jsonData, &result); err != nil { + t.Fatalf("failed to unmarshal JSON: %v", err) + } + + if result["type"] != "connection" { + t.Errorf("expected JSON type 'connection', got %v", result["type"]) + } + + if result["pid"] != float64(1234) { // JSON numbers are float64 + t.Errorf("expected JSON PID 1234, got %v", result["pid"]) + } +} + +// TestChannelStream tests the ChannelStream implementation +func TestChannelStream(t *testing.T) { + stream := NewChannelStream(2) // buffered channel + + // Test that we can get the events channel + eventsChan := stream.Events() + if eventsChan == nil { + t.Fatal("expected non-nil events channel") + } + + // Create some test events + event1 := NewBaseEvent("test1", 100, "cmd1", 1000, map[string]interface{}{"test": 1}) + event2 := NewBaseEvent("test2", 200, "cmd2", 2000, map[string]interface{}{"test": 2}) + + // Send events directly to the underlying channel (for testing) + // In real usage, you'd use the Send method + go func() { + stream.events <- event1 + stream.events <- event2 + stream.Close() + }() + + // Collect events + var receivedEvents []core.Event + for event := range stream.Events() { + receivedEvents = append(receivedEvents, event) + } + + // Verify events + if len(receivedEvents) != 2 { + t.Errorf("expected 2 events, got %d", len(receivedEvents)) + } + + if receivedEvents[0].Type() != "test1" { + t.Errorf("expected first event type 'test1', got %s", receivedEvents[0].Type()) + } + + if receivedEvents[1].Type() != "test2" { + t.Errorf("expected second event type 'test2', got %s", receivedEvents[1].Type()) + } +} + +// TestChannelStreamSend tests sending events to ChannelStream +func TestChannelStreamSend(t *testing.T) { + stream := NewChannelStream(1) + + event := NewBaseEvent("test", 123, "cmd", 1000, map[string]interface{}{}) + + // Test sending + if !stream.Send(event) { + t.Fatal("failed to send event to stream") + } + + // Receive the event + select { + case receivedEvent := <-stream.Events(): + if receivedEvent.Type() != "test" { + t.Errorf("expected event type 'test', got %s", receivedEvent.Type()) + } + case <-time.After(100 * time.Millisecond): + t.Error("timeout waiting for event") + } + + // Close and test sending fails + stream.Close() + + if stream.Send(event) { + t.Error("expected sending to closed stream to return false") + } +} + +// TestMergedStreamBasic tests basic MergedStream functionality +func TestMergedStreamBasic(t *testing.T) { + // Create a simple test without complex concurrency + source := NewChannelStream(5) + streams := []core.EventStream{source} + merged := NewMergedStream(streams) + + // Send one event + event := NewBaseEvent("test", 123, "cmd", 1000, map[string]interface{}{}) + if !source.Send(event) { + t.Fatal("failed to send event") + } + + // Receive the event with timeout + select { + case receivedEvent := <-merged.Events(): + if receivedEvent.Type() != "test" { + t.Errorf("expected event type 'test', got %s", receivedEvent.Type()) + } + case <-time.After(500 * time.Millisecond): + t.Error("timeout waiting for event from merged stream") + } + + // Clean up + source.Close() + merged.Close() +} + +// TestMergedStreamMultipleSources tests merging from multiple sources +func TestMergedStreamMultipleSources(t *testing.T) { + // Create two source streams + source1 := NewChannelStream(5) + source2 := NewChannelStream(5) + streams := []core.EventStream{source1, source2} + merged := NewMergedStream(streams) + + // Send events from both sources + event1 := NewBaseEvent("type1", 1, "cmd1", 1000, map[string]interface{}{}) + event2 := NewBaseEvent("type2", 2, "cmd2", 2000, map[string]interface{}{}) + + source1.Send(event1) + source2.Send(event2) + + // Collect events with timeout + var events []core.Event + timeout := time.After(1 * time.Second) + eventCount := 0 + + for eventCount < 2 { + select { + case event := <-merged.Events(): + events = append(events, event) + eventCount++ + case <-timeout: + t.Fatalf("timeout waiting for events, got %d of 2", eventCount) + } + } + + // Verify we got both events + if len(events) != 2 { + t.Errorf("expected 2 events, got %d", len(events)) + } + + types := make(map[string]bool) + for _, event := range events { + types[event.Type()] = true + } + + if !types["type1"] || !types["type2"] { + t.Error("missing expected event types") + } + + // Clean up + source1.Close() + source2.Close() + merged.Close() +} + +// TestBaseEventUniqueIDs tests that BaseEvent generates unique IDs +func TestBaseEventUniqueIDs(t *testing.T) { + const numEvents = 1000 + ids := make(map[string]bool) + + for i := 0; i < numEvents; i++ { + event := NewBaseEvent("test", uint32(i), "cmd", uint64(i), map[string]interface{}{}) + id := event.ID() + + if ids[id] { + t.Errorf("duplicate ID generated: %s", id) + } + ids[id] = true + + if len(id) == 0 { + t.Error("empty ID generated") + } + } + + if len(ids) != numEvents { + t.Errorf("expected %d unique IDs, got %d", numEvents, len(ids)) + } +} + +// TestBaseEventJSONRoundTrip tests JSON marshaling and unmarshaling +func TestBaseEventJSONRoundTrip(t *testing.T) { + originalMetadata := map[string]interface{}{ + "string_field": "value", + "int_field": 42, + "bool_field": true, + "float_field": 3.14, + } + + original := NewBaseEvent("test_type", 9999, "test_command", 123456789, originalMetadata) + + // Marshal to JSON + jsonData, err := original.MarshalJSON() + if err != nil { + t.Fatalf("failed to marshal to JSON: %v", err) + } + + // Unmarshal back + var result map[string]interface{} + if err := json.Unmarshal(jsonData, &result); err != nil { + t.Fatalf("failed to unmarshal JSON: %v", err) + } + + // Verify fields + if result["type"] != "test_type" { + t.Errorf("expected type 'test_type', got %v", result["type"]) + } + + if result["pid"] != float64(9999) { + t.Errorf("expected PID 9999, got %v", result["pid"]) + } + + if result["command"] != "test_command" { + t.Errorf("expected command 'test_command', got %v", result["command"]) + } + + if result["timestamp"] != float64(123456789) { + t.Errorf("expected timestamp 123456789, got %v", result["timestamp"]) + } + + // Verify metadata (fields are merged at root level, not nested) + if result["string_field"] != "value" { + t.Errorf("expected string_field 'value', got %v", result["string_field"]) + } + + if result["int_field"] != float64(42) { + t.Errorf("expected int_field 42, got %v", result["int_field"]) + } + + if result["bool_field"] != true { + t.Errorf("expected bool_field true, got %v", result["bool_field"]) + } + + if result["float_field"] != 3.14 { + t.Errorf("expected float_field 3.14, got %v", result["float_field"]) + } +} + +// TestChannelStreamCapacity tests buffer capacity behavior +func TestChannelStreamCapacity(t *testing.T) { + stream := NewChannelStream(1) // capacity of 1 + + event1 := NewBaseEvent("test1", 1, "cmd", 1000, map[string]interface{}{}) + event2 := NewBaseEvent("test2", 2, "cmd", 2000, map[string]interface{}{}) + + // Send first event (should succeed) + if !stream.Send(event1) { + t.Fatal("failed to send first event") + } + + // Second event may fail if buffer is full (non-blocking Send) + // This is expected behavior for non-blocking send + result := stream.Send(event2) + t.Logf("Send result for second event (when buffer might be full): %v", result) + + // Consume the first event to make space + select { + case receivedEvent := <-stream.Events(): + if receivedEvent.Type() != "test1" { + t.Errorf("expected first event type 'test1', got %s", receivedEvent.Type()) + } + case <-time.After(100 * time.Millisecond): + t.Error("timeout waiting for first event") + } + + // Now second event should succeed + if !stream.Send(event2) { + t.Error("failed to send second event after consuming first") + } + + // Consume second event + select { + case receivedEvent := <-stream.Events(): + if receivedEvent.Type() != "test2" { + t.Errorf("expected second event type 'test2', got %s", receivedEvent.Type()) + } + case <-time.After(100 * time.Millisecond): + t.Error("timeout waiting for second event") + } + + stream.Close() +} + +// TestTimestampConversion tests the eBPF timestamp to wall-clock time conversion +func TestTimestampConversion(t *testing.T) { + // Reset boot time cache for testing + ResetBootTimeCache() + + // Test with a known timestamp + testTimestamp := uint64(5000000000) // 5 seconds since boot in nanoseconds + + event := NewBaseEvent("test", 1234, "test", testTimestamp, nil) + + // The converted time should be reasonable (not zero, not in the future) + eventTime := event.Time() + if eventTime.IsZero() { + t.Error("converted time should not be zero") + } + + // The event time should be in the past (assuming system has been up for more than 5 seconds) + if eventTime.After(time.Now()) { + t.Error("converted time should not be in the future") + } + + // Test that the timestamp is preserved + if event.Timestamp() != testTimestamp { + t.Errorf("expected timestamp %d, got %d", testTimestamp, event.Timestamp()) + } +} + +// TestBootTimeCalculation tests the boot time calculation mechanism +func TestBootTimeCalculation(t *testing.T) { + // Reset boot time cache + ResetBootTimeCache() + + // First calculation + bootTime1 := calculateSystemBootTime() + if bootTime1.IsZero() { + t.Error("boot time should not be zero") + } + + // Second calculation should return cached value + bootTime2 := calculateSystemBootTime() + if !bootTime1.Equal(bootTime2) { + t.Error("boot time should be cached and consistent") + } + + // Boot time should be in the past + if bootTime1.After(time.Now()) { + t.Error("boot time should be in the past") + } +} + +// TestEBPFTimestampConversion tests the core timestamp conversion function +func TestEBPFTimestampConversion(t *testing.T) { + // Reset boot time cache + ResetBootTimeCache() + + // Test with zero timestamp (should equal boot time) + zeroTime := convertEBPFTimestamp(0) + bootTime := calculateSystemBootTime() + + // Zero timestamp should equal boot time + if !zeroTime.Equal(bootTime) { + t.Errorf("zero timestamp should equal boot time, got %v, expected %v", zeroTime, bootTime) + } + + // Test with 1 second offset + oneSecond := convertEBPFTimestamp(1000000000) // 1 second in nanoseconds + expectedTime := bootTime.Add(time.Second) + + if !oneSecond.Equal(expectedTime) { + t.Errorf("1 second timestamp incorrect, got %v, expected %v", oneSecond, expectedTime) + } +} diff --git a/internal/programs/base.go b/internal/programs/base.go new file mode 100644 index 0000000..3c4d7fb --- /dev/null +++ b/internal/programs/base.go @@ -0,0 +1,288 @@ +// Package programs provides the base implementation for eBPF programs. +package programs + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/cilium/ebpf" + "github.com/cilium/ebpf/link" + "github.com/cilium/ebpf/ringbuf" + "github.com/srodi/ebpf-server/internal/core" + "github.com/srodi/ebpf-server/internal/events" + "github.com/srodi/ebpf-server/pkg/logger" +) + +// Constants for backpressure handling +const ( + // AlertThreshold: Alert when drop rate exceeds 1% over a window + DropRateAlertThreshold = 0.01 + // Alert interval: Don't spam alerts more than once per minute + AlertCooldownDuration = time.Minute + // Event window for calculating drop rate + DropRateWindowSize = 1000 +) + +// BaseProgram provides common functionality for eBPF programs. +type BaseProgram struct { + name string + description string + objectPath string + collection *ebpf.Collection + links []link.Link + eventStream *events.ChannelStream + loaded bool + attached bool + mu sync.RWMutex + + // Event processing metrics + droppedEvents uint64 + totalEvents uint64 + lastAlertTime time.Time +} + +// NewBaseProgram creates a new base program. +func NewBaseProgram(name, description, objectPath string) *BaseProgram { + return &BaseProgram{ + name: name, + description: description, + objectPath: objectPath, + eventStream: events.NewChannelStream(1000), + links: make([]link.Link, 0), + } +} + +// Name returns the program name. +func (p *BaseProgram) Name() string { + return p.name +} + +// Description returns the program description. +func (p *BaseProgram) Description() string { + return p.description +} + +// Load compiles and loads the eBPF program. +func (p *BaseProgram) Load(ctx context.Context) error { + p.mu.Lock() + defer p.mu.Unlock() + + if p.loaded { + return nil + } + + logger.Debugf("Loading eBPF program %s from %s", p.name, p.objectPath) + + collection, err := ebpf.LoadCollection(p.objectPath) + if err != nil { + return fmt.Errorf("failed to load eBPF collection: %w", err) + } + + p.collection = collection + p.loaded = true + + logger.Debugf("Successfully loaded eBPF program %s", p.name) + return nil +} + +// IsLoaded returns true if the program is loaded. +func (p *BaseProgram) IsLoaded() bool { + p.mu.RLock() + defer p.mu.RUnlock() + return p.loaded +} + +// IsAttached returns true if the program is attached. +func (p *BaseProgram) IsAttached() bool { + p.mu.RLock() + defer p.mu.RUnlock() + return p.attached +} + +// EventStream returns the program's event stream. +func (p *BaseProgram) EventStream() core.EventStream { + return p.eventStream +} + +// GetStats returns event processing statistics. +func (p *BaseProgram) GetStats() (totalEvents, droppedEvents uint64, dropRate float64) { + p.mu.RLock() + defer p.mu.RUnlock() + + totalEvents = p.totalEvents + droppedEvents = p.droppedEvents + + if totalEvents > 0 { + dropRate = float64(droppedEvents) / float64(totalEvents) + } + + return totalEvents, droppedEvents, dropRate +} + +// checkDropRateAndAlert monitors drop rate and triggers alerts when necessary. +func (p *BaseProgram) checkDropRateAndAlert() { + total, dropped, dropRate := p.GetStats() + + // Only check if we have enough events to make a meaningful assessment + if total < DropRateWindowSize { + return + } + + // Check if drop rate exceeds threshold and we haven't alerted recently + if dropRate > DropRateAlertThreshold { + now := time.Now() + if now.Sub(p.lastAlertTime) > AlertCooldownDuration { + logger.Errorf("HIGH EVENT DROP RATE DETECTED in %s: %.2f%% (%d/%d events dropped). "+ + "This indicates system overload or insufficient buffer capacity. "+ + "Consider increasing buffer sizes or optimizing event processing.", + p.name, dropRate*100, dropped, total) + + p.mu.Lock() + p.lastAlertTime = now + p.mu.Unlock() + } + } +} + +// Detach detaches the program from all kernel hooks. +func (p *BaseProgram) Detach(ctx context.Context) error { + p.mu.Lock() + defer p.mu.Unlock() + + if !p.attached { + return nil + } + + // Close all links + for _, l := range p.links { + if err := l.Close(); err != nil { + logger.Errorf("Error closing link for program %s: %v", p.name, err) + } + } + + p.links = p.links[:0] + p.attached = false + + // Close event stream + p.eventStream.Close() + + // Reset event statistics + p.droppedEvents = 0 + p.totalEvents = 0 + p.lastAlertTime = time.Time{} + + logger.Debugf("Detached program %s", p.name) + return nil +} + +// GetCollection returns the eBPF collection (for subclasses). +func (p *BaseProgram) GetCollection() *ebpf.Collection { + p.mu.RLock() + defer p.mu.RUnlock() + return p.collection +} + +// AddLink adds a link to track (for subclasses). +func (p *BaseProgram) AddLink(l link.Link) { + p.mu.Lock() + defer p.mu.Unlock() + p.links = append(p.links, l) + p.attached = true +} + +// StartRingBufferReader starts reading from a ring buffer map and parsing events. +func (p *BaseProgram) StartRingBufferReader(mapName string, parser core.EventParser) error { + collection := p.GetCollection() + if collection == nil { + return fmt.Errorf("program not loaded") + } + + ringbufMap := collection.Maps[mapName] + if ringbufMap == nil { + return fmt.Errorf("ring buffer map %s not found", mapName) + } + + logger.Debugf("Starting ring buffer reader for map %s in program %s", mapName, p.name) + + reader, err := ringbuf.NewReader(ringbufMap) + if err != nil { + return fmt.Errorf("failed to create ring buffer reader: %w", err) + } + + // Start reading in a goroutine + go func() { + defer reader.Close() + defer logger.Debugf("Ring buffer reader stopped for %s", p.name) + + for { + record, err := reader.Read() + if err != nil { + if err == ringbuf.ErrClosed { + return + } + logger.Errorf("Error reading from ring buffer in %s: %v", p.name, err) + continue + } + + // Parse the event + event, err := parser.Parse(record.RawSample) + if err != nil { + logger.Errorf("Error parsing event in %s: %v", p.name, err) + continue + } + + // Track total events + p.mu.Lock() + p.totalEvents++ + currentTotal := p.totalEvents + p.mu.Unlock() + + // Send to event stream with backpressure handling + if !p.eventStream.Send(event) { + // Event dropped due to full buffer - this is a critical issue + p.mu.Lock() + p.droppedEvents++ + droppedCount := p.droppedEvents + p.mu.Unlock() + + // Log error (not just debug) since this represents data loss + logger.Errorf("Event stream full for %s, DROPPED EVENT (PID: %d, Type: %s). "+ + "Total dropped: %d/%d events. This indicates backpressure - "+ + "consider increasing buffer size or optimizing downstream processing.", + p.name, event.PID(), event.Type(), droppedCount, currentTotal) + + // Check if we need to trigger high drop rate alerts + p.checkDropRateAndAlert() + } + } + }() + + return nil +} + +// AttachToTracepoint attaches a program to a tracepoint. +func (p *BaseProgram) AttachToTracepoint(progName, group, name string) error { + collection := p.GetCollection() + if collection == nil { + return fmt.Errorf("program not loaded") + } + + prog := collection.Programs[progName] + if prog == nil { + return fmt.Errorf("program %s not found in collection", progName) + } + + logger.Debugf("Attaching program %s to tracepoint %s:%s", progName, group, name) + + l, err := link.Tracepoint(group, name, prog, nil) + if err != nil { + return fmt.Errorf("failed to attach to tracepoint %s:%s: %w", group, name, err) + } + + p.AddLink(l) + logger.Debugf("Successfully attached program %s to tracepoint %s:%s", progName, group, name) + + return nil +} diff --git a/internal/programs/connection/connection.go b/internal/programs/connection/connection.go new file mode 100644 index 0000000..4762771 --- /dev/null +++ b/internal/programs/connection/connection.go @@ -0,0 +1,234 @@ +// Package connection implements eBPF monitoring for network connections. +package connection + +import ( + "context" + "encoding/binary" + "fmt" + "net" + + "github.com/srodi/ebpf-server/internal/core" + "github.com/srodi/ebpf-server/internal/events" + "github.com/srodi/ebpf-server/internal/programs" + "github.com/srodi/ebpf-server/pkg/logger" +) + +const ( + // Program configuration + ProgramName = "connection" + ProgramDescription = "Monitors network connection attempts via sys_enter_connect tracepoint" + ObjectPath = "bpf/connection.o" + + // eBPF program and map names + TracepointProgram = "trace_connect" + EventsMapName = "events" + + // Tracepoint configuration + TracepointGroup = "syscalls" + TracepointName = "sys_enter_connect" +) + +// Program implements the connection monitoring eBPF program. +type Program struct { + *programs.BaseProgram +} + +// NewProgram creates a new connection monitoring program. +func NewProgram() *Program { + base := programs.NewBaseProgram(ProgramName, ProgramDescription, ObjectPath) + return &Program{ + BaseProgram: base, + } +} + +// Attach attaches the program to the appropriate kernel hooks. +func (p *Program) Attach(ctx context.Context) error { + if !p.IsLoaded() { + return fmt.Errorf("program not loaded") + } + + logger.Debugf("Attaching connection monitoring program") + + // Attach to sys_enter_connect tracepoint + if err := p.AttachToTracepoint(TracepointProgram, TracepointGroup, TracepointName); err != nil { + return fmt.Errorf("failed to attach to tracepoint: %w", err) + } + + // Start ring buffer reader + parser := NewEventParser() + if err := p.StartRingBufferReader(EventsMapName, parser); err != nil { + return fmt.Errorf("failed to start ring buffer reader: %w", err) + } + + logger.Info("Connection monitoring program attached and active") + return nil +} + +// EventParser parses connection events from binary data. +type EventParser struct{} + +// NewEventParser creates a new connection event parser. +func NewEventParser() *EventParser { + return &EventParser{} +} + +// EventType returns the type of events this parser handles. +func (p *EventParser) EventType() string { + return "connection" +} + +// Parse converts raw bytes from eBPF into a connection event. +func (p *EventParser) Parse(data []byte) (core.Event, error) { + if len(data) != 60 { + return nil, fmt.Errorf("invalid connection event size: expected 60 bytes, got %d", len(data)) + } + + // Parse binary data based on C struct layout: + // struct event_t { + // u32 pid; // 0-3 + // u64 ts; // 4-11 + // u32 ret; // 12-15 + // char comm[16]; // 16-31 + // u32 dest_ip; // 32-35 + // u8 dest_ip6[16]; // 36-51 + // u16 dest_port; // 52-53 + // u16 family; // 54-55 + // u8 protocol; // 56 + // u8 sock_type; // 57 + // u16 padding; // 58-59 + // } + + pid := binary.LittleEndian.Uint32(data[0:4]) + timestamp := binary.LittleEndian.Uint64(data[4:12]) + ret := int32(binary.LittleEndian.Uint32(data[12:16])) + + // Extract command (null-terminated string) + command := extractNullTerminatedString(data[16:32]) + + destIPv4 := binary.LittleEndian.Uint32(data[32:36]) + var destIPv6 [16]byte + copy(destIPv6[:], data[36:52]) + + destPort := binary.LittleEndian.Uint16(data[52:54]) + family := binary.LittleEndian.Uint16(data[54:56]) + protocol := data[56] + sockType := data[57] + + // Build metadata with parsed fields and derived information + metadata := map[string]interface{}{ + "return_code": ret, + "destination_ip": formatIP(family, destIPv4, destIPv6), + "destination_port": destPort, + "destination": formatDestination(family, destIPv4, destIPv6, destPort), + "address_family": family, + "protocol": formatProtocol(protocol), + "socket_type": formatSocketType(sockType), + + // Raw values for further processing if needed + "raw_ipv4": destIPv4, + "raw_ipv6": destIPv6, + "raw_protocol": protocol, + "raw_socktype": sockType, + } + + event := events.NewBaseEvent("connection", pid, command, timestamp, metadata) + + // Debug log the parsed connection event + destination := formatDestination(family, destIPv4, destIPv6, destPort) + if destination != "" { + logger.Debugf("🔗 CONNECTION EVENT: PID=%d cmd=%s dest=%s proto=%s ret=%d", + pid, command, destination, formatProtocol(protocol), ret) + } else { + logger.Debugf("🔗 CONNECTION EVENT: PID=%d cmd=%s family=%d (local socket) ret=%d", + pid, command, family, ret) + } + + return event, nil +} + +// extractNullTerminatedString extracts a null-terminated string from a byte slice. +func extractNullTerminatedString(data []byte) string { + for i, b := range data { + if b == 0 { + return string(data[:i]) + } + } + return string(data) +} + +// formatIP converts the IP address to a string representation. +func formatIP(family uint16, ipv4 uint32, ipv6 [16]byte) string { + const ( + AF_INET = 2 + AF_INET6 = 10 + ) + + switch family { + case AF_INET: + if ipv4 == 0 { + return "" + } + // Convert from little-endian uint32 to IP address + ip := net.IPv4(byte(ipv4), byte(ipv4>>8), byte(ipv4>>16), byte(ipv4>>24)) + return ip.String() + + case AF_INET6: + // Check if IPv6 address is all zeros + allZero := true + for _, b := range ipv6 { + if b != 0 { + allZero = false + break + } + } + if allZero { + return "" + } + ip := net.IP(ipv6[:]) + return ip.String() + + default: + return "" + } +} + +// formatDestination formats the destination as "IP:port". +func formatDestination(family uint16, ipv4 uint32, ipv6 [16]byte, port uint16) string { + const AF_INET6 = 10 + + ip := formatIP(family, ipv4, ipv6) + if ip == "" { + return "" + } + + // IPv6 addresses need to be wrapped in brackets + if family == AF_INET6 { + return fmt.Sprintf("[%s]:%d", ip, port) + } + + return fmt.Sprintf("%s:%d", ip, port) +} + +// formatProtocol converts protocol number to string. +func formatProtocol(protocol uint8) string { + switch protocol { + case 6: + return "TCP" + case 17: + return "UDP" + default: + return fmt.Sprintf("Unknown(%d)", protocol) + } +} + +// formatSocketType converts socket type to string. +func formatSocketType(sockType uint8) string { + switch sockType { + case 1: + return "STREAM" + case 2: + return "DGRAM" + default: + return fmt.Sprintf("Unknown(%d)", sockType) + } +} diff --git a/internal/programs/connection/connection_test.go b/internal/programs/connection/connection_test.go new file mode 100644 index 0000000..565a834 --- /dev/null +++ b/internal/programs/connection/connection_test.go @@ -0,0 +1,341 @@ +package connection + +import ( + "encoding/binary" + "testing" +) + +// TestProgram tests the Program struct +func TestProgram(t *testing.T) { + program := NewProgram() + + // Test basic properties + if program.Name() != "connection" { + t.Errorf("expected name 'connection', got %s", program.Name()) + } + + if program.Description() != "Monitors network connection attempts via sys_enter_connect tracepoint" { + t.Errorf("unexpected description: %s", program.Description()) + } + + // Test initial state + if program.IsLoaded() { + t.Error("program should not be loaded initially") + } + + if program.IsAttached() { + t.Error("program should not be attached initially") + } + + // Test event stream + stream := program.EventStream() + if stream == nil { + t.Error("expected non-nil event stream") + } +} + +// TestEventParser tests the EventParser struct +func TestEventParser(t *testing.T) { + parser := NewEventParser() + + // Test event type + if parser.EventType() != "connection" { + t.Errorf("expected event type 'connection', got %s", parser.EventType()) + } +} + +// TestParseValidConnectionEvent tests parsing of valid binary event data +func TestParseValidConnectionEvent(t *testing.T) { + parser := NewEventParser() + + // Create test binary data (60 bytes total as per the C struct) + testData := make([]byte, 60) + + // Set test values based on C struct layout: + // struct event_t { + // u32 pid; // 0-3 + // u64 ts; // 4-11 + // u32 ret; // 12-15 + // char comm[16]; // 16-31 + // u32 dest_ip; // 32-35 + // u8 dest_ip6[16]; // 36-51 + // u16 dest_port; // 52-53 + // u16 family; // 54-55 + // u8 protocol; // 56 + // u8 sock_type; // 57 + // u16 padding; // 58-59 + // } + + // pid (offset 0, 4 bytes) + binary.LittleEndian.PutUint32(testData[0:4], 1234) + + // timestamp (offset 4, 8 bytes) + binary.LittleEndian.PutUint64(testData[4:12], 1000000) + + // ret (offset 12, 4 bytes) + binary.LittleEndian.PutUint32(testData[12:16], 0) // Success + + // command (offset 16, 16 bytes) + copy(testData[16:32], []byte("curl\x00")) + + // dest_ip (offset 32, 4 bytes) - 127.0.0.1 + // Need to store as little-endian but the IP extraction expects different byte order + binary.LittleEndian.PutUint32(testData[32:36], 0x0100007f) + + // dest_ip6 (offset 36, 16 bytes) - leave as zeros for IPv4 + + // dest_port (offset 52, 2 bytes) + binary.LittleEndian.PutUint16(testData[52:54], 80) + + // family (offset 54, 2 bytes) - AF_INET = 2 + binary.LittleEndian.PutUint16(testData[54:56], 2) + + // protocol (offset 56, 1 byte) - TCP = 6 + testData[56] = 6 + + // sock_type (offset 57, 1 byte) - STREAM = 1 + testData[57] = 1 + + // Test parsing + event, err := parser.Parse(testData) + if err != nil { + t.Fatalf("failed to parse binary data: %v", err) + } + + // Verify parsed event + if event.Type() != "connection" { + t.Errorf("expected type 'connection', got %s", event.Type()) + } + + if event.PID() != 1234 { + t.Errorf("expected PID 1234, got %d", event.PID()) + } + + if event.Command() != "curl" { + t.Errorf("expected command 'curl', got %s", event.Command()) + } + + if event.Timestamp() != 1000000 { + t.Errorf("expected timestamp 1000000, got %d", event.Timestamp()) + } + + // Check metadata + metadata := event.Metadata() + + if metadata["protocol"] != "TCP" { + t.Errorf("expected protocol 'TCP', got %v", metadata["protocol"]) + } + + if metadata["destination_ip"] != "127.0.0.1" { + t.Errorf("expected destination_ip '127.0.0.1', got %v", metadata["destination_ip"]) + } + + if metadata["destination_port"] != uint16(80) { + t.Errorf("expected destination_port 80, got %v", metadata["destination_port"]) + } + + if metadata["destination"] != "127.0.0.1:80" { + t.Errorf("expected destination '127.0.0.1:80', got %v", metadata["destination"]) + } + + if metadata["return_code"] != int32(0) { + t.Errorf("expected return_code 0, got %v", metadata["return_code"]) + } + + if metadata["address_family"] != uint16(2) { + t.Errorf("expected address_family 2 (AF_INET), got %v", metadata["address_family"]) + } + + if metadata["socket_type"] != "STREAM" { + t.Errorf("expected socket_type 'STREAM', got %v", metadata["socket_type"]) + } +} + +// TestParseIPv6ConnectionEvent tests parsing of IPv6 connection events +func TestParseIPv6ConnectionEvent(t *testing.T) { + parser := NewEventParser() + testData := make([]byte, 60) + + // Set basic fields + binary.LittleEndian.PutUint32(testData[0:4], 5678) // pid + binary.LittleEndian.PutUint64(testData[4:12], 2000000) // timestamp + binary.LittleEndian.PutUint32(testData[12:16], 0) // ret + copy(testData[16:32], []byte("wget\x00")) // command + + // IPv4 dest_ip = 0 (not used for IPv6) + binary.LittleEndian.PutUint32(testData[32:36], 0) + + // IPv6 address: ::1 (localhost) + ipv6 := [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1} + copy(testData[36:52], ipv6[:]) + + binary.LittleEndian.PutUint16(testData[52:54], 443) // dest_port (HTTPS) + binary.LittleEndian.PutUint16(testData[54:56], 10) // family (AF_INET6 = 10) + testData[56] = 6 // protocol (TCP) + testData[57] = 1 // sock_type (STREAM) + + event, err := parser.Parse(testData) + if err != nil { + t.Fatalf("failed to parse IPv6 binary data: %v", err) + } + + metadata := event.Metadata() + + if metadata["destination_ip"] != "::1" { + t.Errorf("expected destination_ip '::1', got %v", metadata["destination_ip"]) + } + + if metadata["destination"] != "[::1]:443" { + t.Errorf("expected destination '[::1]:443', got %v", metadata["destination"]) + } + + if metadata["address_family"] != uint16(10) { + t.Errorf("expected address_family 10 (AF_INET6), got %v", metadata["address_family"]) + } +} + +// TestParseLocalSocketEvent tests parsing of local socket events (no IP) +func TestParseLocalSocketEvent(t *testing.T) { + parser := NewEventParser() + testData := make([]byte, 60) + + // Set basic fields + binary.LittleEndian.PutUint32(testData[0:4], 9999) // pid + binary.LittleEndian.PutUint64(testData[4:12], 3000000) // timestamp + binary.LittleEndian.PutUint32(testData[12:16], 0xFFFFFFFF) // ret (error -1) + copy(testData[16:32], []byte("test\x00")) // command + + // No IP addresses (all zeros) + // family = 1 (AF_UNIX), no destination info + binary.LittleEndian.PutUint16(testData[54:56], 1) // family (AF_UNIX) + testData[56] = 0 // protocol + testData[57] = 1 // sock_type (STREAM) + + event, err := parser.Parse(testData) + if err != nil { + t.Fatalf("failed to parse local socket data: %v", err) + } + + metadata := event.Metadata() + + // For local sockets, IP should be empty + if metadata["destination_ip"] != "" { + t.Errorf("expected empty destination_ip for local socket, got %v", metadata["destination_ip"]) + } + + if metadata["destination"] != "" { + t.Errorf("expected empty destination for local socket, got %v", metadata["destination"]) + } + + if metadata["return_code"] != int32(-1) { + t.Errorf("expected return_code -1, got %v", metadata["return_code"]) + } +} + +// TestParseInvalidData tests parsing with invalid data +func TestParseInvalidData(t *testing.T) { + parser := NewEventParser() + + // Test with data that's too short + shortData := make([]byte, 10) + _, err := parser.Parse(shortData) + if err == nil { + t.Error("expected error when parsing data that's too short") + } + + // Test with data that's too long + longData := make([]byte, 100) + _, err = parser.Parse(longData) + if err == nil { + t.Error("expected error when parsing data that's too long") + } + + // Test with nil data + _, err = parser.Parse(nil) + if err == nil { + t.Error("expected error when parsing nil data") + } + + // Test with empty data + _, err = parser.Parse([]byte{}) + if err == nil { + t.Error("expected error when parsing empty data") + } +} + +// TestFormatProtocol tests protocol formatting +func TestFormatProtocol(t *testing.T) { + testCases := []struct { + protocol uint8 + expected string + }{ + {6, "TCP"}, + {17, "UDP"}, + {255, "Unknown(255)"}, + } + + for _, tc := range testCases { + result := formatProtocol(tc.protocol) + if result != tc.expected { + t.Errorf("protocol %d: expected '%s', got '%s'", tc.protocol, tc.expected, result) + } + } +} + +// TestFormatSocketType tests socket type formatting +func TestFormatSocketType(t *testing.T) { + testCases := []struct { + sockType uint8 + expected string + }{ + {1, "STREAM"}, + {2, "DGRAM"}, + {255, "Unknown(255)"}, + } + + for _, tc := range testCases { + result := formatSocketType(tc.sockType) + if result != tc.expected { + t.Errorf("socket type %d: expected '%s', got '%s'", tc.sockType, tc.expected, result) + } + } +} + +// TestExtractNullTerminatedString tests string extraction +func TestExtractNullTerminatedString(t *testing.T) { + testCases := []struct { + name string + input []byte + expected string + }{ + { + name: "normal string", + input: []byte("hello\x00world"), + expected: "hello", + }, + { + name: "no null terminator", + input: []byte("hello"), + expected: "hello", + }, + { + name: "empty string", + input: []byte("\x00abc"), + expected: "", + }, + { + name: "all zeros", + input: []byte{0, 0, 0, 0}, + expected: "", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := extractNullTerminatedString(tc.input) + if result != tc.expected { + t.Errorf("expected '%s', got '%s'", tc.expected, result) + } + }) + } +} diff --git a/internal/programs/manager.go b/internal/programs/manager.go new file mode 100644 index 0000000..9985074 --- /dev/null +++ b/internal/programs/manager.go @@ -0,0 +1,179 @@ +// Package manager provides program management functionality. +package programs + +import ( + "context" + "fmt" + "sync" + + "github.com/srodi/ebpf-server/internal/core" + "github.com/srodi/ebpf-server/internal/events" + "github.com/srodi/ebpf-server/pkg/logger" +) + +// Manager orchestrates multiple eBPF programs. +type Manager struct { + programs []core.Program + eventStream *events.MergedStream + running bool + mu sync.RWMutex +} + +// NewManager creates a new program manager. +func NewManager() *Manager { + return &Manager{ + programs: make([]core.Program, 0), + } +} + +// RegisterProgram adds a program to the manager. +func (m *Manager) RegisterProgram(program core.Program) error { + m.mu.Lock() + defer m.mu.Unlock() + + // Check for nil program + if program == nil { + return fmt.Errorf("cannot register nil program") + } + + // Check for duplicate names + for _, p := range m.programs { + if p.Name() == program.Name() { + return fmt.Errorf("program with name %s already registered", program.Name()) + } + } + + m.programs = append(m.programs, program) + logger.Debugf("Registered program: %s", program.Name()) + + return nil +} + +// LoadAll loads all registered programs. +func (m *Manager) LoadAll(ctx context.Context) error { + m.mu.Lock() + defer m.mu.Unlock() + + logger.Debugf("Loading %d eBPF programs", len(m.programs)) + + for _, program := range m.programs { + if err := program.Load(ctx); err != nil { + return fmt.Errorf("failed to load program %s: %w", program.Name(), err) + } + } + + logger.Info("All eBPF programs loaded successfully") + return nil +} + +// AttachAll attaches all loaded programs. +func (m *Manager) AttachAll(ctx context.Context) error { + m.mu.Lock() + defer m.mu.Unlock() + + logger.Debugf("Attaching %d eBPF programs", len(m.programs)) + + // Collect event streams from all programs + var streams []core.EventStream + + for _, program := range m.programs { + if !program.IsLoaded() { + return fmt.Errorf("program %s is not loaded", program.Name()) + } + + if err := program.Attach(ctx); err != nil { + return fmt.Errorf("failed to attach program %s: %w", program.Name(), err) + } + + streams = append(streams, program.EventStream()) + } + + // Create merged event stream + m.eventStream = events.NewMergedStream(streams) + m.running = true + + logger.Info("All eBPF programs attached successfully") + return nil +} + +// DetachAll detaches all programs. +func (m *Manager) DetachAll(ctx context.Context) error { + m.mu.Lock() + defer m.mu.Unlock() + + if !m.running { + return nil + } + + logger.Debugf("Detaching %d eBPF programs", len(m.programs)) + + // Close merged event stream + if m.eventStream != nil { + m.eventStream.Close() + m.eventStream = nil + } + + // Detach all programs + for _, program := range m.programs { + if err := program.Detach(ctx); err != nil { + logger.Errorf("Error detaching program %s: %v", program.Name(), err) + } + } + + m.running = false + logger.Info("All eBPF programs detached") + + return nil +} + +// Programs returns all registered programs. +func (m *Manager) Programs() []core.Program { + m.mu.RLock() + defer m.mu.RUnlock() + + // Return a copy to prevent external modifications + programs := make([]core.Program, len(m.programs)) + copy(programs, m.programs) + + return programs +} + +// EventStream returns the unified event stream. +func (m *Manager) EventStream() core.EventStream { + m.mu.RLock() + defer m.mu.RUnlock() + + return m.eventStream +} + +// IsRunning returns true if the manager is active. +func (m *Manager) IsRunning() bool { + m.mu.RLock() + defer m.mu.RUnlock() + + return m.running +} + +// GetProgramStatus returns the status of all programs. +func (m *Manager) GetProgramStatus() []core.ProgramStatus { + m.mu.RLock() + defer m.mu.RUnlock() + + status := make([]core.ProgramStatus, len(m.programs)) + + for i, program := range m.programs { + totalEvents, droppedEvents, dropRate := program.GetStats() + + status[i] = core.ProgramStatus{ + Name: program.Name(), + Description: program.Description(), + Loaded: program.IsLoaded(), + Attached: program.IsAttached(), + EventCount: int64(totalEvents), + DroppedCount: int64(droppedEvents), + DropRate: dropRate, + } + } + + return status +} diff --git a/internal/programs/manager_test.go b/internal/programs/manager_test.go new file mode 100644 index 0000000..fa7fe7e --- /dev/null +++ b/internal/programs/manager_test.go @@ -0,0 +1,354 @@ +package programs + +import ( + "context" + "fmt" + "testing" + + "github.com/srodi/ebpf-server/internal/core" + "github.com/srodi/ebpf-server/internal/events" +) + +// MockProgram for testing the Manager +type MockProgram struct { + name string + description string + loaded bool + attached bool + stream *events.ChannelStream +} + +func (m *MockProgram) Name() string { return m.name } +func (m *MockProgram) Description() string { return m.description } +func (m *MockProgram) Load(ctx context.Context) error { m.loaded = true; return nil } +func (m *MockProgram) Attach(ctx context.Context) error { m.attached = true; return nil } +func (m *MockProgram) Detach(ctx context.Context) error { m.attached = false; return nil } +func (m *MockProgram) IsLoaded() bool { return m.loaded } +func (m *MockProgram) IsAttached() bool { return m.attached } +func (m *MockProgram) EventStream() core.EventStream { return m.stream } +func (m *MockProgram) GetStats() (uint64, uint64, float64) { return 0, 0, 0.0 } + +// TestManagerBasic tests basic Manager functionality +func TestManagerBasic(t *testing.T) { + manager := NewManager() + + if manager.IsRunning() { + t.Error("manager should not be running initially") + } + + // Test empty manager + programs := manager.Programs() + if len(programs) != 0 { + t.Errorf("expected 0 programs initially, got %d", len(programs)) + } + + status := manager.GetProgramStatus() + if len(status) != 0 { + t.Errorf("expected 0 program status initially, got %d", len(status)) + } +} + +// TestManagerRegisterProgram tests program registration +func TestManagerRegisterProgram(t *testing.T) { + manager := NewManager() + + // Create mock programs + stream1 := events.NewChannelStream(10) + stream2 := events.NewChannelStream(10) + + program1 := &MockProgram{ + name: "connection", + description: "Connection monitoring program", + stream: stream1, + } + + program2 := &MockProgram{ + name: "packet_drop", + description: "Packet drop monitoring program", + stream: stream2, + } + + // Register programs + if err := manager.RegisterProgram(program1); err != nil { + t.Fatalf("failed to register program1: %v", err) + } + + if err := manager.RegisterProgram(program2); err != nil { + t.Fatalf("failed to register program2: %v", err) + } + + // Verify programs are registered + programs := manager.Programs() + if len(programs) != 2 { + t.Errorf("expected 2 programs, got %d", len(programs)) + } + + // Verify program status + status := manager.GetProgramStatus() + if len(status) != 2 { + t.Errorf("expected 2 program status, got %d", len(status)) + } + + // Find and verify each program status + statusMap := make(map[string]core.ProgramStatus) + for _, s := range status { + statusMap[s.Name] = s + } + + if connStatus, ok := statusMap["connection"]; ok { + if connStatus.Description != "Connection monitoring program" { + t.Errorf("expected connection description, got %s", connStatus.Description) + } + if connStatus.Loaded { + t.Error("connection program should not be loaded initially") + } + if connStatus.Attached { + t.Error("connection program should not be attached initially") + } + } else { + t.Error("connection program status not found") + } +} + +// TestManagerLoadAttachDetach tests the load/attach/detach lifecycle +func TestManagerLoadAttachDetach(t *testing.T) { + manager := NewManager() + ctx := context.Background() + + // Create mock program + stream := events.NewChannelStream(10) + program := &MockProgram{ + name: "test", + description: "Test program", + stream: stream, + } + + if err := manager.RegisterProgram(program); err != nil { + t.Fatalf("failed to register program: %v", err) + } + + // Test LoadAll + if err := manager.LoadAll(ctx); err != nil { + t.Fatalf("failed to load all programs: %v", err) + } + + // Manager should not be running yet (only loaded, not attached) + if manager.IsRunning() { + t.Error("manager should not be running after LoadAll (only loaded, not attached)") + } + + // Verify program is loaded + status := manager.GetProgramStatus() + if len(status) != 1 { + t.Fatalf("expected 1 program status, got %d", len(status)) + } + + if !status[0].Loaded { + t.Error("program should be loaded after LoadAll") + } + + // Test AttachAll + if err := manager.AttachAll(ctx); err != nil { + t.Fatalf("failed to attach all programs: %v", err) + } + + // Manager should be running after AttachAll + if !manager.IsRunning() { + t.Error("manager should be running after AttachAll") + } + + // Verify program is attached + status = manager.GetProgramStatus() + if !status[0].Attached { + t.Error("program should be attached after AttachAll") + } + + // Test DetachAll + if err := manager.DetachAll(ctx); err != nil { + t.Fatalf("failed to detach all programs: %v", err) + } + + // Manager should not be running after DetachAll + if manager.IsRunning() { + t.Error("manager should not be running after DetachAll") + } + + // Verify program is detached + status = manager.GetProgramStatus() + if status[0].Attached { + t.Error("program should be detached after DetachAll") + } +} + +// TestManagerEventStream tests the unified event stream +func TestManagerEventStream(t *testing.T) { + manager := NewManager() + ctx := context.Background() + + // Create mock programs with streams + stream1 := events.NewChannelStream(10) + stream2 := events.NewChannelStream(10) + + program1 := &MockProgram{ + name: "prog1", + stream: stream1, + } + + program2 := &MockProgram{ + name: "prog2", + stream: stream2, + } + + if err := manager.RegisterProgram(program1); err != nil { + t.Fatalf("failed to register program1: %v", err) + } + if err := manager.RegisterProgram(program2); err != nil { + t.Fatalf("failed to register program2: %v", err) + } + + // Load and attach programs to create the unified stream + if err := manager.LoadAll(ctx); err != nil { + t.Fatalf("failed to load programs: %v", err) + } + + if err := manager.AttachAll(ctx); err != nil { + t.Fatalf("failed to attach programs: %v", err) + } + + // Get unified event stream (should now be available) + unifiedStream := manager.EventStream() + if unifiedStream == nil { + t.Fatal("expected non-nil unified event stream after AttachAll") + } + + // Send events to individual streams + event1 := events.NewBaseEvent("type1", 100, "cmd1", 1000, map[string]interface{}{}) + event2 := events.NewBaseEvent("type2", 200, "cmd2", 2000, map[string]interface{}{}) + + stream1.Send(event1) + stream2.Send(event2) + + // Try to receive events from unified stream + // Note: This test might be flaky depending on timing + receivedEvents := 0 + + // Use select with timeout to avoid hanging +eventLoop: + for receivedEvents < 2 { + select { + case event := <-unifiedStream.Events(): + receivedEvents++ + t.Logf("Received event type: %s, PID: %d", event.Type(), event.PID()) + default: + // If no events immediately available, break to avoid hanging + break eventLoop + } + } + + t.Logf("Received %d events from unified stream", receivedEvents) + + // Clean up + if err := manager.DetachAll(ctx); err != nil { + t.Errorf("failed to detach programs: %v", err) + } + stream1.Close() + stream2.Close() +} + +// TestManagerDuplicateRegistration tests handling of duplicate program registration +func TestManagerDuplicateRegistration(t *testing.T) { + manager := NewManager() + + stream := events.NewChannelStream(10) + program := &MockProgram{ + name: "test", + stream: stream, + } + + // Register program first time + if err := manager.RegisterProgram(program); err != nil { + t.Fatalf("failed to register program first time: %v", err) + } + + // Register same program again - should return error + if err := manager.RegisterProgram(program); err == nil { + t.Error("expected error when registering duplicate program") + } + + // Verify only one program is registered + programs := manager.Programs() + if len(programs) != 1 { + t.Errorf("expected 1 program after duplicate registration, got %d", len(programs)) + } +} + +// TestManagerWithNilProgram tests handling of nil program registration +func TestManagerWithNilProgram(t *testing.T) { + manager := NewManager() + + // Try to register nil program + if err := manager.RegisterProgram(nil); err == nil { + t.Error("expected error when registering nil program") + } + + // Verify no programs are registered + programs := manager.Programs() + if len(programs) != 0 { + t.Errorf("expected 0 programs after nil registration, got %d", len(programs)) + } +} + +// TestManagerConcurrency tests concurrent access to manager +func TestManagerConcurrency(t *testing.T) { + manager := NewManager() + + // Register programs from multiple goroutines + numGoroutines := 10 + errors := make(chan error, numGoroutines) + + for i := 0; i < numGoroutines; i++ { + go func(id int) { + stream := events.NewChannelStream(10) + program := &MockProgram{ + name: fmt.Sprintf("program_%d", id), + stream: stream, + } + + errors <- manager.RegisterProgram(program) + }(i) + } + + // Collect errors + for i := 0; i < numGoroutines; i++ { + if err := <-errors; err != nil { + t.Errorf("concurrent registration failed: %v", err) + } + } + + // Verify all programs are registered + programs := manager.Programs() + if len(programs) != numGoroutines { + t.Errorf("expected %d programs, got %d", numGoroutines, len(programs)) + } + + // Test concurrent status queries instead of conflicting operations + statusErrors := make(chan error, numGoroutines) + + for i := 0; i < numGoroutines; i++ { + go func() { + status := manager.GetProgramStatus() + if len(status) != numGoroutines { + statusErrors <- fmt.Errorf("expected %d status entries, got %d", numGoroutines, len(status)) + } else { + statusErrors <- nil + } + }() + } + + // Collect status errors + for i := 0; i < numGoroutines; i++ { + if err := <-statusErrors; err != nil { + t.Errorf("concurrent status query failed: %v", err) + } + } +} diff --git a/internal/programs/packet_drop/packet_drop.go b/internal/programs/packet_drop/packet_drop.go new file mode 100644 index 0000000..7f6fae1 --- /dev/null +++ b/internal/programs/packet_drop/packet_drop.go @@ -0,0 +1,147 @@ +// Package packet_drop implements eBPF monitoring for packet drops. +package packet_drop + +import ( + "context" + "encoding/binary" + "fmt" + + "github.com/srodi/ebpf-server/internal/core" + "github.com/srodi/ebpf-server/internal/events" + "github.com/srodi/ebpf-server/internal/programs" + "github.com/srodi/ebpf-server/pkg/logger" +) + +const ( + // Program configuration + ProgramName = "packet_drop" + ProgramDescription = "Monitors packet drops via kfree_skb tracepoint" + ObjectPath = "bpf/packet_drop.o" + + // eBPF program and map names + TracepointProgram = "trace_kfree_skb" + EventsMapName = "drop_events" + + // Tracepoint configuration + TracepointGroup = "skb" + TracepointName = "kfree_skb" +) + +// Program implements the packet drop monitoring eBPF program. +type Program struct { + *programs.BaseProgram +} + +// NewProgram creates a new packet drop monitoring program. +func NewProgram() *Program { + base := programs.NewBaseProgram(ProgramName, ProgramDescription, ObjectPath) + return &Program{ + BaseProgram: base, + } +} + +// Attach attaches the program to the appropriate kernel hooks. +func (p *Program) Attach(ctx context.Context) error { + if !p.IsLoaded() { + return fmt.Errorf("program not loaded") + } + + logger.Debugf("Attaching packet drop monitoring program") + + // Attach to kfree_skb tracepoint + if err := p.AttachToTracepoint(TracepointProgram, TracepointGroup, TracepointName); err != nil { + return fmt.Errorf("failed to attach to tracepoint: %w", err) + } + + // Start ring buffer reader + parser := NewEventParser() + if err := p.StartRingBufferReader(EventsMapName, parser); err != nil { + return fmt.Errorf("failed to start ring buffer reader: %w", err) + } + + logger.Info("Packet drop monitoring program attached and active") + return nil +} + +// EventParser parses packet drop events from binary data. +type EventParser struct{} + +// NewEventParser creates a new packet drop event parser. +func NewEventParser() *EventParser { + return &EventParser{} +} + +// EventType returns the type of events this parser handles. +func (p *EventParser) EventType() string { + return "packet_drop" +} + +// Parse converts raw bytes from eBPF into a packet drop event. +func (p *EventParser) Parse(data []byte) (core.Event, error) { + if len(data) != 44 { + return nil, fmt.Errorf("invalid packet drop event size: expected 44 bytes, got %d", len(data)) + } + + // Parse binary data based on C struct layout: + // struct drop_event_t { + // u32 pid; // 0-3 + // u64 ts; // 4-11 + // char comm[16]; // 12-27 + // u32 drop_reason; // 28-31 + // u32 skb_len; // 32-35 + // u8 padding[8]; // 36-43 + // } + + pid := binary.LittleEndian.Uint32(data[0:4]) + timestamp := binary.LittleEndian.Uint64(data[4:12]) + + // Extract command (null-terminated string) + command := extractNullTerminatedString(data[12:28]) + + dropReason := binary.LittleEndian.Uint32(data[28:32]) + skbLen := binary.LittleEndian.Uint32(data[32:36]) + + // Build metadata with parsed fields and derived information + metadata := map[string]interface{}{ + "drop_reason_code": dropReason, + "drop_reason": formatDropReason(dropReason), + "skb_length": skbLen, + "packet_size_bytes": skbLen, + } + + event := events.NewBaseEvent("packet_drop", pid, command, timestamp, metadata) + + // Debug log the parsed packet drop event + logger.Debugf("📦 PACKET DROP EVENT: PID=%d cmd=%s reason=%s (%d) size=%d bytes", + pid, command, formatDropReason(dropReason), dropReason, skbLen) + + return event, nil +} + +// extractNullTerminatedString extracts a null-terminated string from a byte slice. +func extractNullTerminatedString(data []byte) string { + for i, b := range data { + if b == 0 { + return string(data[:i]) + } + } + return string(data) +} + +// formatDropReason converts drop reason code to human-readable string. +func formatDropReason(reason uint32) string { + switch reason { + case 1: + return "SKB_FREE" + case 2: + return "TCP_DROP" + case 3: + return "UDP_DROP" + case 4: + return "ICMP_DROP" + case 5: + return "NETFILTER_DROP" + default: + return fmt.Sprintf("UNKNOWN(%d)", reason) + } +} diff --git a/internal/programs/packet_drop/packet_drop_test.go b/internal/programs/packet_drop/packet_drop_test.go new file mode 100644 index 0000000..49c3819 --- /dev/null +++ b/internal/programs/packet_drop/packet_drop_test.go @@ -0,0 +1,380 @@ +package packet_drop + +import ( + "encoding/binary" + "testing" +) + +// TestProgram tests the Program struct +func TestProgram(t *testing.T) { + program := NewProgram() + + // Test basic properties + if program.Name() != "packet_drop" { + t.Errorf("expected name 'packet_drop', got %s", program.Name()) + } + + if program.Description() != "Monitors packet drops via kfree_skb tracepoint" { + t.Errorf("unexpected description: %s", program.Description()) + } + + // Test initial state + if program.IsLoaded() { + t.Error("program should not be loaded initially") + } + + if program.IsAttached() { + t.Error("program should not be attached initially") + } + + // Test event stream + stream := program.EventStream() + if stream == nil { + t.Error("expected non-nil event stream") + } +} + +// TestEventParser tests the EventParser struct +func TestEventParser(t *testing.T) { + parser := NewEventParser() + + // Test event type + if parser.EventType() != "packet_drop" { + t.Errorf("expected event type 'packet_drop', got %s", parser.EventType()) + } +} + +// TestParseValidPacketDropEvent tests parsing of valid binary event data +func TestParseValidPacketDropEvent(t *testing.T) { + parser := NewEventParser() + + // Create test binary data (44 bytes total as per the C struct) + testData := make([]byte, 44) + + // Set test values based on C struct layout: + // struct drop_event_t { + // u32 pid; // 0-3 + // u64 ts; // 4-11 + // char comm[16]; // 12-27 + // u32 drop_reason; // 28-31 + // u32 skb_len; // 32-35 + // u8 padding[8]; // 36-43 + // } + + // pid (offset 0, 4 bytes) + binary.LittleEndian.PutUint32(testData[0:4], 5678) + + // timestamp (offset 4, 8 bytes) + binary.LittleEndian.PutUint64(testData[4:12], 2000000) + + // command (offset 12, 16 bytes) + copy(testData[12:28], []byte("iptables\x00")) + + // drop_reason (offset 28, 4 bytes) + binary.LittleEndian.PutUint32(testData[28:32], 2) // TCP_DROP + + // skb_len (offset 32, 4 bytes) + binary.LittleEndian.PutUint32(testData[32:36], 1500) + + // Test parsing + event, err := parser.Parse(testData) + if err != nil { + t.Fatalf("failed to parse binary data: %v", err) + } + + // Verify parsed event + if event.Type() != "packet_drop" { + t.Errorf("expected type 'packet_drop', got %s", event.Type()) + } + + if event.PID() != 5678 { + t.Errorf("expected PID 5678, got %d", event.PID()) + } + + if event.Command() != "iptables" { + t.Errorf("expected command 'iptables', got %s", event.Command()) + } + + if event.Timestamp() != 2000000 { + t.Errorf("expected timestamp 2000000, got %d", event.Timestamp()) + } + + // Check metadata + metadata := event.Metadata() + + if metadata["drop_reason_code"] != uint32(2) { + t.Errorf("expected drop_reason_code 2, got %v", metadata["drop_reason_code"]) + } + + if metadata["drop_reason"] != "TCP_DROP" { + t.Errorf("expected drop_reason 'TCP_DROP', got %v", metadata["drop_reason"]) + } + + if metadata["skb_length"] != uint32(1500) { + t.Errorf("expected skb_length 1500, got %v", metadata["skb_length"]) + } + + if metadata["packet_size_bytes"] != uint32(1500) { + t.Errorf("expected packet_size_bytes 1500, got %v", metadata["packet_size_bytes"]) + } +} + +// TestParsePacketDropWithDifferentReasons tests parsing with various drop reasons +func TestParsePacketDropWithDifferentReasons(t *testing.T) { + parser := NewEventParser() + + testCases := []struct { + name string + reasonCode uint32 + expectedName string + }{ + {"SKB_FREE", 1, "SKB_FREE"}, + {"TCP_DROP", 2, "TCP_DROP"}, + {"UDP_DROP", 3, "UDP_DROP"}, + {"ICMP_DROP", 4, "ICMP_DROP"}, + {"NETFILTER_DROP", 5, "NETFILTER_DROP"}, + {"Unknown reason", 99, "UNKNOWN(99)"}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testData := make([]byte, 44) + + // Set basic fields + binary.LittleEndian.PutUint32(testData[0:4], 1234) // pid + binary.LittleEndian.PutUint64(testData[4:12], 1000000) // timestamp + copy(testData[12:28], []byte("test\x00")) // command + binary.LittleEndian.PutUint32(testData[28:32], tc.reasonCode) // drop_reason + binary.LittleEndian.PutUint32(testData[32:36], 500) // skb_len + + event, err := parser.Parse(testData) + if err != nil { + t.Fatalf("failed to parse data for %s: %v", tc.name, err) + } + + metadata := event.Metadata() + if metadata["drop_reason"] != tc.expectedName { + t.Errorf("expected drop_reason '%s', got %v", tc.expectedName, metadata["drop_reason"]) + } + + if metadata["drop_reason_code"] != tc.reasonCode { + t.Errorf("expected drop_reason_code %d, got %v", tc.reasonCode, metadata["drop_reason_code"]) + } + }) + } +} + +// TestParsePacketDropWithDifferentSizes tests parsing with various packet sizes +func TestParsePacketDropWithDifferentSizes(t *testing.T) { + parser := NewEventParser() + + testCases := []struct { + name string + size uint32 + }{ + {"Small packet", 64}, + {"Medium packet", 1024}, + {"Large packet", 1500}, + {"Jumbo frame", 9000}, + {"Zero size", 0}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testData := make([]byte, 44) + + // Set basic fields + binary.LittleEndian.PutUint32(testData[0:4], 1234) // pid + binary.LittleEndian.PutUint64(testData[4:12], 1000000) // timestamp + copy(testData[12:28], []byte("test\x00")) // command + binary.LittleEndian.PutUint32(testData[28:32], 1) // drop_reason (SKB_FREE) + binary.LittleEndian.PutUint32(testData[32:36], tc.size) // skb_len + + event, err := parser.Parse(testData) + if err != nil { + t.Fatalf("failed to parse data for %s: %v", tc.name, err) + } + + metadata := event.Metadata() + if metadata["skb_length"] != tc.size { + t.Errorf("expected skb_length %d, got %v", tc.size, metadata["skb_length"]) + } + + if metadata["packet_size_bytes"] != tc.size { + t.Errorf("expected packet_size_bytes %d, got %v", tc.size, metadata["packet_size_bytes"]) + } + }) + } +} + +// TestParseInvalidData tests parsing with invalid data +func TestParseInvalidData(t *testing.T) { + parser := NewEventParser() + + // Test with data that's too short + shortData := make([]byte, 10) + _, err := parser.Parse(shortData) + if err == nil { + t.Error("expected error when parsing data that's too short") + } + + // Test with data that's too long + longData := make([]byte, 100) + _, err = parser.Parse(longData) + if err == nil { + t.Error("expected error when parsing data that's too long") + } + + // Test with nil data + _, err = parser.Parse(nil) + if err == nil { + t.Error("expected error when parsing nil data") + } + + // Test with empty data + _, err = parser.Parse([]byte{}) + if err == nil { + t.Error("expected error when parsing empty data") + } +} + +// TestFormatDropReason tests drop reason formatting +func TestFormatDropReason(t *testing.T) { + testCases := []struct { + reason uint32 + expected string + }{ + {1, "SKB_FREE"}, + {2, "TCP_DROP"}, + {3, "UDP_DROP"}, + {4, "ICMP_DROP"}, + {5, "NETFILTER_DROP"}, + {0, "UNKNOWN(0)"}, + {999, "UNKNOWN(999)"}, + } + + for _, tc := range testCases { + result := formatDropReason(tc.reason) + if result != tc.expected { + t.Errorf("reason %d: expected '%s', got '%s'", tc.reason, tc.expected, result) + } + } +} + +// TestExtractNullTerminatedString tests string extraction +func TestExtractNullTerminatedString(t *testing.T) { + testCases := []struct { + name string + input []byte + expected string + }{ + { + name: "normal string", + input: []byte("iptables\x00world"), + expected: "iptables", + }, + { + name: "no null terminator", + input: []byte("firewall"), + expected: "firewall", + }, + { + name: "empty string", + input: []byte("\x00abc"), + expected: "", + }, + { + name: "kernel command", + input: []byte("kworker/0:1\x00"), + expected: "kworker/0:1", + }, + { + name: "all zeros", + input: []byte{0, 0, 0, 0}, + expected: "", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := extractNullTerminatedString(tc.input) + if result != tc.expected { + t.Errorf("expected '%s', got '%s'", tc.expected, result) + } + }) + } +} + +// TestPacketDropEventCompleteScenarios tests complete realistic scenarios +func TestPacketDropEventCompleteScenarios(t *testing.T) { + parser := NewEventParser() + + scenarios := []struct { + name string + pid uint32 + command string + reason uint32 + size uint32 + expectedMsg string + }{ + { + name: "Firewall dropped large packet", + pid: 1234, + command: "iptables", + reason: 5, // NETFILTER_DROP + size: 1500, + expectedMsg: "NETFILTER_DROP", + }, + { + name: "TCP connection dropped", + pid: 5678, + command: "nginx", + reason: 2, // TCP_DROP + size: 1024, + expectedMsg: "TCP_DROP", + }, + { + name: "UDP packet dropped", + pid: 9999, + command: "systemd-resolve", + reason: 3, // UDP_DROP + size: 512, + expectedMsg: "UDP_DROP", + }, + } + + for _, scenario := range scenarios { + t.Run(scenario.name, func(t *testing.T) { + testData := make([]byte, 44) + + binary.LittleEndian.PutUint32(testData[0:4], scenario.pid) + binary.LittleEndian.PutUint64(testData[4:12], 3000000) + copy(testData[12:28], []byte(scenario.command+"\x00")) + binary.LittleEndian.PutUint32(testData[28:32], scenario.reason) + binary.LittleEndian.PutUint32(testData[32:36], scenario.size) + + event, err := parser.Parse(testData) + if err != nil { + t.Fatalf("failed to parse %s: %v", scenario.name, err) + } + + // Verify all expected fields + if event.PID() != scenario.pid { + t.Errorf("expected PID %d, got %d", scenario.pid, event.PID()) + } + + if event.Command() != scenario.command { + t.Errorf("expected command '%s', got '%s'", scenario.command, event.Command()) + } + + metadata := event.Metadata() + if metadata["drop_reason"] != scenario.expectedMsg { + t.Errorf("expected drop_reason '%s', got %v", scenario.expectedMsg, metadata["drop_reason"]) + } + + if metadata["packet_size_bytes"] != scenario.size { + t.Errorf("expected packet_size_bytes %d, got %v", scenario.size, metadata["packet_size_bytes"]) + } + }) + } +} diff --git a/internal/storage/memory.go b/internal/storage/memory.go new file mode 100644 index 0000000..85ee8bd --- /dev/null +++ b/internal/storage/memory.go @@ -0,0 +1,202 @@ +// Package storage provides event storage implementations. +package storage + +import ( + "context" + "sort" + "sync" + "time" + + "github.com/srodi/ebpf-server/internal/core" + "github.com/srodi/ebpf-server/pkg/logger" +) + +// MemoryStorage implements EventSink using in-memory storage. +// This is suitable for development and small-scale deployments. +type MemoryStorage struct { + events []core.Event + mu sync.RWMutex +} + +// NewMemoryStorage creates a new in-memory event storage. +func NewMemoryStorage() *MemoryStorage { + return &MemoryStorage{ + events: make([]core.Event, 0), + } +} + +// Store saves an event to memory. +func (s *MemoryStorage) Store(ctx context.Context, event core.Event) error { + s.mu.Lock() + defer s.mu.Unlock() + + s.events = append(s.events, event) + + // Debug log stored events + logger.Debugf("💾 STORED EVENT: type=%s PID=%d cmd=%s total_events=%d", + event.Type(), event.PID(), event.Command(), len(s.events)) + + return nil +} + +// Query retrieves events matching the criteria. +func (s *MemoryStorage) Query(ctx context.Context, query core.Query) ([]core.Event, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + var results []core.Event + + for _, event := range s.events { + if s.matchesQuery(event, query) { + results = append(results, event) + } + } + + // Sort by timestamp (most recent first) + sort.Slice(results, func(i, j int) bool { + return results[i].Timestamp() > results[j].Timestamp() + }) + + // Apply limit + if query.Limit > 0 && len(results) > query.Limit { + results = results[:query.Limit] + } + + return results, nil +} + +// Count returns the number of events matching the criteria. +func (s *MemoryStorage) Count(ctx context.Context, query core.Query) (int, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + count := 0 + for _, event := range s.events { + if s.matchesQuery(event, query) { + count++ + } + } + + return count, nil +} + +// matchesQuery checks if an event matches the query criteria. +func (s *MemoryStorage) matchesQuery(event core.Event, query core.Query) bool { + // Filter by event type + if query.EventType != "" && event.Type() != query.EventType { + return false + } + + // Filter by PID + if query.PID != 0 && event.PID() != query.PID { + return false + } + + // Filter by command + if query.Command != "" && event.Command() != query.Command { + return false + } + + // Filter by time range + eventTime := event.Time() + if !query.Since.IsZero() && eventTime.Before(query.Since) { + return false + } + if !query.Until.IsZero() && eventTime.After(query.Until) { + return false + } + + return true +} + +// Cleanup removes old events (for memory management). +func (s *MemoryStorage) Cleanup(maxAge time.Duration) { + s.mu.Lock() + defer s.mu.Unlock() + + cutoff := time.Now().Add(-maxAge) + var kept []core.Event + + for _, event := range s.events { + if event.Time().After(cutoff) { + kept = append(kept, event) + } + } + + s.events = kept +} + +// StorageWithSink wraps a storage implementation and automatically stores events from a stream. +type StorageWithSink struct { + storage core.EventSink + stream core.EventStream + ctx context.Context + cancel context.CancelFunc +} + +// NewStorageWithSink creates storage that automatically consumes from an event stream. +func NewStorageWithSink(storage core.EventSink, stream core.EventStream) *StorageWithSink { + ctx, cancel := context.WithCancel(context.Background()) + + s := &StorageWithSink{ + storage: storage, + stream: stream, + ctx: ctx, + cancel: cancel, + } + + // Start consuming events + go s.consumeEvents() + + return s +} + +// Store implements EventSink. +func (s *StorageWithSink) Store(ctx context.Context, event core.Event) error { + return s.storage.Store(ctx, event) +} + +// Query implements EventSink. +func (s *StorageWithSink) Query(ctx context.Context, query core.Query) ([]core.Event, error) { + return s.storage.Query(ctx, query) +} + +// Count implements EventSink. +func (s *StorageWithSink) Count(ctx context.Context, query core.Query) (int, error) { + return s.storage.Count(ctx, query) +} + +// Close stops consuming events. +func (s *StorageWithSink) Close() error { + s.cancel() + return s.stream.Close() +} + +// consumeEvents reads events from the stream and stores them. +func (s *StorageWithSink) consumeEvents() { + for { + select { + case event, ok := <-s.stream.Events(): + if !ok { + return + } + + if err := s.storage.Store(s.ctx, event); err != nil { + // Log the storage error - this could indicate memory pressure, + // disk space issues, or other critical storage problems + logger.Errorf("Failed to store event (PID: %d, Type: %s): %v", + event.PID(), event.Type(), err) + + // For critical storage failures, we continue processing to avoid + // blocking the event stream, but log the error for monitoring + // In production, consider implementing: + // - Metrics/alerting for storage failure rates + // - Circuit breaker for persistent failures + // - Backup storage mechanisms + } + + case <-s.ctx.Done(): + return + } + } +} diff --git a/internal/storage/memory_test.go b/internal/storage/memory_test.go new file mode 100644 index 0000000..e1c544e --- /dev/null +++ b/internal/storage/memory_test.go @@ -0,0 +1,424 @@ +package storage + +import ( + "context" + "testing" + "time" + + "github.com/srodi/ebpf-server/internal/core" + "github.com/srodi/ebpf-server/internal/events" +) + +// TestMemoryStorageBasic tests basic storage operations +func TestMemoryStorageBasic(t *testing.T) { + storage := NewMemoryStorage() + ctx := context.Background() + + // Create test events + event1 := events.NewBaseEvent("connection", 1234, "curl", 1000, map[string]interface{}{ + "dest": "127.0.0.1:80", + }) + event2 := events.NewBaseEvent("packet_drop", 5678, "nginx", 2000, map[string]interface{}{ + "reason": "SKB_DROP_REASON_NO_SOCKET", + }) + + // Store events + if err := storage.Store(ctx, event1); err != nil { + t.Fatalf("failed to store event1: %v", err) + } + + if err := storage.Store(ctx, event2); err != nil { + t.Fatalf("failed to store event2: %v", err) + } + + // Query all events + allEvents, err := storage.Query(ctx, core.Query{}) + if err != nil { + t.Fatalf("failed to query all events: %v", err) + } + + if len(allEvents) != 2 { + t.Errorf("expected 2 events, got %d", len(allEvents)) + } + + // Count all events + count, err := storage.Count(ctx, core.Query{}) + if err != nil { + t.Fatalf("failed to count all events: %v", err) + } + + if count != 2 { + t.Errorf("expected count 2, got %d", count) + } +} + +// TestMemoryStorageQueryByType tests filtering by event type +func TestMemoryStorageQueryByType(t *testing.T) { + storage := NewMemoryStorage() + ctx := context.Background() + + // Store events of different types + for i := 0; i < 3; i++ { + event := events.NewBaseEvent("connection", uint32(1000+i), "curl", uint64(i), map[string]interface{}{}) + if err := storage.Store(ctx, event); err != nil { + t.Fatalf("failed to store connection event: %v", err) + } + } + + for i := 0; i < 2; i++ { + event := events.NewBaseEvent("packet_drop", uint32(2000+i), "nginx", uint64(10+i), map[string]interface{}{}) + if err := storage.Store(ctx, event); err != nil { + t.Fatalf("failed to store packet_drop event: %v", err) + } + } + + // Query by type + connectionQuery := core.Query{EventType: "connection"} + connectionEvents, err := storage.Query(ctx, connectionQuery) + if err != nil { + t.Fatalf("failed to query connection events: %v", err) + } + + if len(connectionEvents) != 3 { + t.Errorf("expected 3 connection events, got %d", len(connectionEvents)) + } + + // Verify all are connection events + for _, event := range connectionEvents { + if event.Type() != "connection" { + t.Errorf("expected connection event, got %s", event.Type()) + } + } + + // Query packet drop events + dropQuery := core.Query{EventType: "packet_drop"} + dropEvents, err := storage.Query(ctx, dropQuery) + if err != nil { + t.Fatalf("failed to query packet drop events: %v", err) + } + + if len(dropEvents) != 2 { + t.Errorf("expected 2 packet drop events, got %d", len(dropEvents)) + } + + // Count by type + connectionCount, err := storage.Count(ctx, connectionQuery) + if err != nil { + t.Fatalf("failed to count connection events: %v", err) + } + + if connectionCount != 3 { + t.Errorf("expected 3 connection events count, got %d", connectionCount) + } +} + +// TestMemoryStorageQueryByPID tests filtering by process ID +func TestMemoryStorageQueryByPID(t *testing.T) { + storage := NewMemoryStorage() + ctx := context.Background() + + // Store events with different PIDs + pids := []uint32{1234, 5678, 1234, 9999} + for i, pid := range pids { + event := events.NewBaseEvent("connection", pid, "test", uint64(i), map[string]interface{}{}) + if err := storage.Store(ctx, event); err != nil { + t.Fatalf("failed to store event: %v", err) + } + } + + // Query by PID + query := core.Query{PID: 1234} + pidEvents, err := storage.Query(ctx, query) + if err != nil { + t.Fatalf("failed to query events by PID: %v", err) + } + + if len(pidEvents) != 2 { + t.Errorf("expected 2 events for PID 1234, got %d", len(pidEvents)) + } + + // Verify all events have correct PID + for _, event := range pidEvents { + if event.PID() != 1234 { + t.Errorf("expected PID 1234, got %d", event.PID()) + } + } +} + +// TestMemoryStorageQueryByCommand tests filtering by command name +func TestMemoryStorageQueryByCommand(t *testing.T) { + storage := NewMemoryStorage() + ctx := context.Background() + + // Store events with different commands + commands := []string{"curl", "wget", "curl", "nginx"} + for i, cmd := range commands { + event := events.NewBaseEvent("connection", uint32(1000+i), cmd, uint64(i), map[string]interface{}{}) + if err := storage.Store(ctx, event); err != nil { + t.Fatalf("failed to store event: %v", err) + } + } + + // Query by command + query := core.Query{Command: "curl"} + curlEvents, err := storage.Query(ctx, query) + if err != nil { + t.Fatalf("failed to query events by command: %v", err) + } + + if len(curlEvents) != 2 { + t.Errorf("expected 2 events for command 'curl', got %d", len(curlEvents)) + } + + // Verify all events have correct command + for _, event := range curlEvents { + if event.Command() != "curl" { + t.Errorf("expected command 'curl', got %s", event.Command()) + } + } +} + +// TestMemoryStorageQueryByTimeRange tests filtering by time range +func TestMemoryStorageQueryByTimeRange(t *testing.T) { + storage := NewMemoryStorage() + ctx := context.Background() + + now := time.Now() + + // Store events with different times + times := []time.Time{ + now.Add(-2 * time.Hour), + now.Add(-1 * time.Hour), + now.Add(-30 * time.Minute), + now.Add(-10 * time.Minute), + now, + } + + for i := range times { + // Create event with custom time + event := events.NewBaseEvent("connection", uint32(1000+i), "curl", uint64(i), map[string]interface{}{}) + // Override the time (this is a bit of a hack for testing) + // In practice, events would have their time set based on kernel timestamp + if err := storage.Store(ctx, event); err != nil { + t.Fatalf("failed to store event: %v", err) + } + } + + // Query events since 1 hour ago + query := core.Query{Since: now.Add(-1 * time.Hour)} + recentEvents, err := storage.Query(ctx, query) + if err != nil { + t.Fatalf("failed to query recent events: %v", err) + } + + // Note: This test might not work as expected because BaseEvent + // calculates time based on kernel timestamp, not our custom time. + // We're primarily testing that the query doesn't crash. + t.Logf("Found %d recent events (time-based filtering may be approximate)", len(recentEvents)) + + // Query events until 30 minutes ago + query = core.Query{Until: now.Add(-30 * time.Minute)} + oldEvents, err := storage.Query(ctx, query) + if err != nil { + t.Fatalf("failed to query old events: %v", err) + } + + t.Logf("Found %d old events", len(oldEvents)) +} + +// TestMemoryStorageQueryWithLimit tests result limiting +func TestMemoryStorageQueryWithLimit(t *testing.T) { + storage := NewMemoryStorage() + ctx := context.Background() + + // Store many events + for i := 0; i < 20; i++ { + event := events.NewBaseEvent("connection", uint32(1000+i), "curl", uint64(i), map[string]interface{}{}) + if err := storage.Store(ctx, event); err != nil { + t.Fatalf("failed to store event: %v", err) + } + } + + // Query with limit + query := core.Query{Limit: 5} + limitedEvents, err := storage.Query(ctx, query) + if err != nil { + t.Fatalf("failed to query with limit: %v", err) + } + + if len(limitedEvents) != 5 { + t.Errorf("expected 5 events with limit, got %d", len(limitedEvents)) + } + + // Query without limit should return all + query = core.Query{} + allEvents, err := storage.Query(ctx, query) + if err != nil { + t.Fatalf("failed to query all events: %v", err) + } + + if len(allEvents) != 20 { + t.Errorf("expected 20 events without limit, got %d", len(allEvents)) + } +} + +// TestMemoryStorageComplexQuery tests combining multiple query criteria +func TestMemoryStorageComplexQuery(t *testing.T) { + storage := NewMemoryStorage() + ctx := context.Background() + + // Store diverse events + testData := []struct { + eventType string + pid uint32 + command string + }{ + {"connection", 1234, "curl"}, + {"connection", 5678, "curl"}, + {"connection", 1234, "wget"}, + {"packet_drop", 1234, "curl"}, + {"packet_drop", 5678, "nginx"}, + } + + for i, data := range testData { + event := events.NewBaseEvent(data.eventType, data.pid, data.command, uint64(i), map[string]interface{}{}) + if err := storage.Store(ctx, event); err != nil { + t.Fatalf("failed to store event: %v", err) + } + } + + // Complex query: connection events from PID 1234 with command "curl" + query := core.Query{ + EventType: "connection", + PID: 1234, + Command: "curl", + } + + results, err := storage.Query(ctx, query) + if err != nil { + t.Fatalf("failed to execute complex query: %v", err) + } + + if len(results) != 1 { + t.Errorf("expected 1 result for complex query, got %d", len(results)) + } + + // Verify the result matches all criteria + if len(results) > 0 { + event := results[0] + if event.Type() != "connection" { + t.Errorf("expected connection event, got %s", event.Type()) + } + if event.PID() != 1234 { + t.Errorf("expected PID 1234, got %d", event.PID()) + } + if event.Command() != "curl" { + t.Errorf("expected command 'curl', got %s", event.Command()) + } + } +} + +// TestMemoryStorageConcurrency tests concurrent access to storage +func TestMemoryStorageConcurrency(t *testing.T) { + storage := NewMemoryStorage() + ctx := context.Background() + + // Number of goroutines and events per goroutine + numGoroutines := 10 + eventsPerGoroutine := 50 + + // Channel to collect errors + errors := make(chan error, numGoroutines*2) + + // Start writers + for i := 0; i < numGoroutines; i++ { + go func(id int) { + for j := 0; j < eventsPerGoroutine; j++ { + event := events.NewBaseEvent("connection", uint32(id), "test", uint64(j), map[string]interface{}{ + "goroutine": id, + "sequence": j, + }) + if err := storage.Store(ctx, event); err != nil { + errors <- err + return + } + } + errors <- nil + }(i) + } + + // Start readers + for i := 0; i < numGoroutines; i++ { + go func(id int) { + query := core.Query{PID: uint32(id)} + if _, err := storage.Query(ctx, query); err != nil { + errors <- err + return + } + if _, err := storage.Count(ctx, query); err != nil { + errors <- err + return + } + errors <- nil + }(i) + } + + // Wait for all goroutines and check for errors + for i := 0; i < numGoroutines*2; i++ { + if err := <-errors; err != nil { + t.Fatalf("concurrent operation failed: %v", err) + } + } + + // Verify final state + totalEvents, err := storage.Count(ctx, core.Query{}) + if err != nil { + t.Fatalf("failed to count total events: %v", err) + } + + expectedTotal := numGoroutines * eventsPerGoroutine + if totalEvents != expectedTotal { + t.Errorf("expected %d total events, got %d", expectedTotal, totalEvents) + } +} + +// TestMemoryStorageEventOrdering tests that events are returned in the correct order +func TestMemoryStorageEventOrdering(t *testing.T) { + storage := NewMemoryStorage() + ctx := context.Background() + + // Store events with increasing timestamps + var expectedOrder []string + for i := 0; i < 5; i++ { + eventID := "event_" + string(rune('A'+i)) + event := events.NewBaseEvent("test", uint32(i), eventID, uint64(i*1000), map[string]interface{}{}) + if err := storage.Store(ctx, event); err != nil { + t.Fatalf("failed to store event: %v", err) + } + expectedOrder = append(expectedOrder, eventID) + } + + // Query all events + allEvents, err := storage.Query(ctx, core.Query{}) + if err != nil { + t.Fatalf("failed to query events: %v", err) + } + + // Verify order (should be by timestamp - newest first) + if len(allEvents) != 5 { + t.Errorf("expected 5 events, got %d", len(allEvents)) + } + + // Note: The actual ordering depends on the implementation + // Let's just verify we get all events back + commandsSeen := make(map[string]bool) + for _, event := range allEvents { + commandsSeen[event.Command()] = true + } + + for _, expectedCommand := range expectedOrder { + if !commandsSeen[expectedCommand] { + t.Errorf("missing event with command %s", expectedCommand) + } + } +} diff --git a/internal/system/system.go b/internal/system/system.go new file mode 100644 index 0000000..b7e7cdc --- /dev/null +++ b/internal/system/system.go @@ -0,0 +1,118 @@ +// Package system provides the main orchestration for the eBPF monitoring system. +package system + +import ( + "context" + "fmt" + + "github.com/srodi/ebpf-server/internal/core" + "github.com/srodi/ebpf-server/internal/programs" + "github.com/srodi/ebpf-server/internal/programs/connection" + "github.com/srodi/ebpf-server/internal/programs/packet_drop" + "github.com/srodi/ebpf-server/internal/storage" + "github.com/srodi/ebpf-server/pkg/logger" +) + +// System is the main orchestrator for the eBPF monitoring system. +type System struct { + manager core.Manager + storage core.EventSink +} + +// NewSystem creates a new eBPF monitoring system. +func NewSystem() *System { + manager := programs.NewManager() + memStorage := storage.NewMemoryStorage() + + return &System{ + manager: manager, + storage: memStorage, + } +} + +// Initialize sets up the system with all available programs. +func (s *System) Initialize() error { + logger.Info("🚀 Initializing eBPF monitoring system") + + // Register connection monitoring program + connProgram := connection.NewProgram() + if err := s.manager.RegisterProgram(connProgram); err != nil { + return fmt.Errorf("failed to register connection program: %w", err) + } + logger.Debugf("✅ Registered connection monitoring program") + + // Register packet drop monitoring program + dropProgram := packet_drop.NewProgram() + if err := s.manager.RegisterProgram(dropProgram); err != nil { + return fmt.Errorf("failed to register packet drop program: %w", err) + } + logger.Debugf("✅ Registered packet drop monitoring program") + + logger.Info("🚀 eBPF monitoring system initialized successfully") + return nil +} + +// Start loads and attaches all programs, then starts event collection. +func (s *System) Start(ctx context.Context) error { + logger.Info("🔧 Starting eBPF monitoring system") + + // Load all programs + if err := s.manager.LoadAll(ctx); err != nil { + return fmt.Errorf("failed to load programs: %w", err) + } + logger.Debugf("✅ All eBPF programs loaded") + + // Attach all programs + if err := s.manager.AttachAll(ctx); err != nil { + return fmt.Errorf("failed to attach programs: %w", err) + } + logger.Debugf("✅ All eBPF programs attached to kernel") + + // Start consuming events and storing them + eventStream := s.manager.EventStream() + if eventStream != nil { + s.storage = storage.NewStorageWithSink(s.storage, eventStream) + logger.Debugf("✅ Event storage pipeline started") + } + + logger.Info("🎯 eBPF monitoring system started and ready to capture events!") + return nil +} + +// Stop detaches all programs and cleans up resources. +func (s *System) Stop(ctx context.Context) error { + logger.Info("Stopping eBPF monitoring system") + + // Stop storage sink + if storageWithSink, ok := s.storage.(*storage.StorageWithSink); ok { + storageWithSink.Close() + } + + // Detach all programs + if err := s.manager.DetachAll(ctx); err != nil { + return fmt.Errorf("failed to detach programs: %w", err) + } + + logger.Info("eBPF monitoring system stopped") + return nil +} + +// IsRunning returns true if the system is active. +func (s *System) IsRunning() bool { + return s.manager.IsRunning() +} + +// GetPrograms returns status of all programs. +func (s *System) GetPrograms() []core.ProgramStatus { + return s.manager.GetProgramStatus() +} + +// QueryEvents retrieves events matching the criteria. +func (s *System) QueryEvents(ctx context.Context, query core.Query) ([]core.Event, error) { + return s.storage.Query(ctx, query) +} + +// CountEvents returns the number of events matching the criteria. +func (s *System) CountEvents(ctx context.Context, query core.Query) (int, error) { + return s.storage.Count(ctx, query) +} diff --git a/server b/server new file mode 100755 index 0000000..6e510a5 Binary files /dev/null and b/server differ