# Ingress Traffic Capture

This guide shows you how to use Qtap to capture **ingress (incoming) HTTP traffic** to your applications. We'll use a simple Python FastAPI web server as an example, but the same principles apply to any HTTP server.

## What You'll Learn

* Configure Qtap to capture incoming HTTP requests
* See request headers, bodies, and metadata from client requests
* Understand the difference between ingress and egress capture
* Monitor API endpoints your application exposes

## Use Cases

**Why capture ingress traffic?**

* Debug API requests from clients
* Monitor what data clients are sending to your endpoints
* Audit incoming traffic for security analysis
* Understand client behavior and usage patterns
* Troubleshoot issues with incoming requests

***

## Prerequisites

* Linux system with kernel 5.10+ and eBPF support
* Python 3.7+ installed
* Qtap installed (see [Installation](https://docs.qpoint.io/getting-started/qtap/installation))
* Root/sudo access

***

## Part 1: The Python Web Server

First, let's create a simple FastAPI web server that we'll monitor.

### Install FastAPI and Uvicorn

```bash
pip install fastapi uvicorn
```

### Create the Web Server

Save this as `api_server.py`:

```python
from fastapi import FastAPI, Header
from typing import Optional
import uvicorn

app = FastAPI()

@app.get("/")
async def root():
    """Simple health check endpoint"""
    return {"message": "API server is running", "status": "healthy"}

@app.get("/api/users/{user_id}")
async def get_user(user_id: int, x_request_id: Optional[str] = Header(None)):
    """Get user by ID with optional request tracking header"""
    return {
        "user_id": user_id,
        "name": f"User {user_id}",
        "email": f"user{user_id}@example.com",
        "request_id": x_request_id
    }

@app.post("/api/users")
async def create_user(user_data: dict):
    """Create a new user"""
    return {
        "message": "User created successfully",
        "data": user_data,
        "id": 12345
    }

@app.get("/api/slow")
async def slow_endpoint():
    """Simulate a slow endpoint"""
    import time
    time.sleep(2)
    return {"message": "This took 2 seconds"}

if __name__ == "__main__":
    # Listen on all interfaces so other containers/machines can reach it
    # This is required for true ingress capture from network sources
    uvicorn.run(app, host="0.0.0.0", port=8000)
```

### Start the Server

In one terminal, start the Python server:

```bash
python api_server.py
```

You should see:

```
INFO:     Started server process [12345]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
```

The server is now listening on all network interfaces, making it accessible from other containers or machines on your network.

***

## Part 2: Qtap Configuration for Ingress

Now let's configure Qtap to capture incoming requests to our Python server.

### Create the Qtap Config

Save this as `qtap-ingress.yaml`:

```yaml
version: 2

# Storage Configuration - Output to console
services:
  # Event metadata (connection info, timing)
  event_stores:
    - type: stdout

  # HTTP request/response data
  object_stores:
    - type: stdout

# Processing Stack - Capture incoming HTTP traffic
stacks:
  ingress_stack:
    plugins:
      # HTTP Capture plugin
      - type: http_capture
        config:
          level: full      # (none|summary|headers|full) - Capture everything including request/response bodies
          format: text     # (json|text) - Human-readable format

# Traffic Capture Settings - INGRESS configuration
tap:
  direction: ingress         # (egress|ingress|all) - Only capture INCOMING traffic
  ignore_loopback: true      # (true|false) - True since we're capturing network traffic (not localhost)
  audit_include_dns: false   # (true|false) - Skip DNS for cleaner output

  http:
    stack: ingress_stack     # Use our ingress processing stack
```

**Key Configuration Points:**

* **`direction: ingress`** - Only captures incoming HTTP requests from the network
* **`ignore_loopback: true`** - We're capturing real network traffic, not localhost
* **`level: full`** - Captures complete request/response including bodies

***

## Part 3: Running Qtap and Testing

### Step 1: Start Qtap (Before Making Requests!)

In a **second terminal**, start Qtap with the ingress config:

```bash
# Using Docker
docker run -d \
  --name qtap-ingress \
  --user 0:0 --privileged \
  --cap-add CAP_BPF --cap-add CAP_SYS_ADMIN \
  --pid=host --network=host \
  -v /sys:/sys \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v "$(pwd)/qtap-ingress.yaml:/app/config/qtap.yaml" \
  -e TINI_SUBREAPER=1 \
  --ulimit=memlock=-1 \
  us-docker.pkg.dev/qpoint-edge/public/qtap:v0 \
  --log-level=info \
  --log-encoding=console \
  --config="/app/config/qtap.yaml"

# Wait for Qtap to initialize (CRITICAL - must wait before traffic!)
sleep 6
```

Or using the Linux binary:

```bash
sudo qtap \
  --config=qtap-ingress.yaml \
  --log-level=info \
  --log-encoding=console
```

### Step 2: Find Your Host's Network IP Address

Find the IP address of the machine running your Python server:

```bash
# On Linux - find your primary network interface
ip addr show | grep "inet " | grep -v 127.0.0.1

# Example output:
# inet 192.168.1.100/24 brd 192.168.1.255 scope global eth0
```

The IP address before the `/` is what you'll use (e.g., `192.168.1.100`).

{% hint style="info" %}
**Note:** You can test from the **same machine** using its network IP (not localhost), or from a **different machine** on the same network. Both demonstrate true ingress traffic.
{% endhint %}

### Step 3: Allow Firewall Access (If Needed)

If you have a firewall enabled, allow traffic on port 8000:

```bash
# On Ubuntu/Debian with ufw
sudo ufw allow 8000/tcp
sudo ufw status

# Or temporarily disable for testing
# sudo ufw disable
```

### Step 4: Generate Test Traffic from Another Host

From **another machine on your network** (or the same machine using its network IP):

```bash
# Replace 192.168.1.100 with your server's IP address

# Test 1: Simple GET request (TRUE INGRESS!)
curl http://192.168.1.100:8000/

# Test 2: GET with path parameter
curl http://192.168.1.100:8000/api/users/42

# Test 3: GET with custom header
curl http://192.168.1.100:8000/api/users/99 \
  -H "X-Request-ID: test-12345"

# Test 4: POST with JSON body
curl -X POST http://192.168.1.100:8000/api/users \
  -H "Content-Type: application/json" \
  -d '{"name": "Alice Smith", "email": "alice@example.com", "role": "admin"}'

# Test 5: Slow endpoint (tests latency tracking)
curl http://192.168.1.100:8000/api/slow
```

{% hint style="success" %}
**Why not use localhost?** Using the network IP (even from the same machine) creates **true ingress traffic** from the network perspective. The kernel treats this as actual incoming network traffic, not loopback.
{% endhint %}

**Alternative: Test from the same machine**

If you don't have another machine available, you can still test from the same host using its network IP:

```bash
# On the SAME machine where Python and Qtap are running
# Use your network IP, NOT 127.0.0.1

curl http://192.168.1.100:8000/  # Replace with your actual IP

# This creates network traffic (goes through the network stack)
# Even though source and destination are the same physical machine
```

### Step 5: View Captured Traffic

Check the Qtap logs to see the captured ingress traffic:

```bash
# If using Docker
docker logs qtap-ingress

# Look for output showing the Python process and incoming requests
```

**What you should see:**

```
=== HTTP Transaction ===
Source Process: python (PID: 12345)
Direction: INGRESS ← (incoming from network)
Source IP: 192.168.1.50 (client machine or same host via network IP)
Destination IP: 192.168.1.100:8000 (your Python server)
Method: POST
URL: http://192.168.1.100:8000/api/users
Status: 200 OK
Duration: 15ms

--- Request Headers ---
Host: 192.168.1.100:8000
User-Agent: curl/7.81.0
Accept: */*
Content-Type: application/json
Content-Length: 67

--- Request Body ---
{"name": "Alice Smith", "email": "alice@example.com", "role": "admin"}

--- Response Headers ---
Content-Type: application/json
Content-Length: 89

--- Response Body ---
{"message":"User created successfully","data":{"name":"Alice Smith","email":"alice@example.com","role":"admin"},"id":12345}
========================
```

**Key indicators that it's working:**

* ✅ `"exe": "/usr/bin/python3"` - Python process identified
* ✅ `Direction: INGRESS` - True incoming traffic from network
* ✅ `Source IP: 192.168.1.50` - **NOT 127.0.0.1** - This is real network traffic!
* ✅ `Destination IP: 192.168.1.100:8000` - Your server's network IP
* ✅ `"protocol": "http1"` - HTTP protocol parsed correctly
* ✅ Full request/response bodies visible
* ✅ Custom headers captured (`X-Request-ID`)
* ✅ Latency/duration tracked

## Part 4: Configuration Variations

### Variation 1: Capture Both Ingress and Egress

To capture both incoming requests AND outgoing requests your Python app makes:

```yaml
tap:
  direction: all             # Capture everything
  ignore_loopback: false     # Include localhost
  http:
    stack: ingress_stack
```

### Variation 2: Headers Only (No Bodies)

To reduce output size, capture only headers:

```yaml
stacks:
  headers_only:
    plugins:
      - type: http_capture
        config:
          level: headers     # (summary|headers|full) - Headers includes headers but not bodies
          format: text
```

### Variation 3: Filter Specific Endpoints with Rules

Capture only POST requests or specific paths using Rulekit:

```yaml
stacks:
  filtered_stack:
    plugins:
      - type: http_capture
        config:
          level: none        # Don't capture by default
          format: json
          rules:
            # Only capture POST requests
            - name: "POST requests only"
              expr: http.req.method == "POST"
              level: full

            # Capture slow requests (> 1 second)
            - name: "Slow endpoints"
              expr: http.res.duration_ms > 1000
              level: full
```

### Variation 4: Production Setup with S3 Storage

For production, send sensitive request/response data to your own S3 bucket:

```yaml
services:
  event_stores:
    - type: stdout          # Metadata to console

  object_stores:
    - type: s3              # Bodies to S3 (never leaves your environment)
      endpoint: s3.amazonaws.com
      region: us-east-1
      bucket: my-company-qtap-ingress
      access_key:
        type: env
        value: AWS_ACCESS_KEY_ID
      secret_key:
        type: env
        value: AWS_SECRET_ACCESS_KEY
      insecure: false
```

See [Level 4 of the Complete Guide](https://docs.qpoint.io/guides/qtap-guides/getting-started-complete-guide#level-4-production-storage-with-s3-20-minutes) for full S3 setup.

***

## Understanding the Output

### What Qtap Captures for Ingress

**Event Metadata (anonymized):**

* Source IP and port (client)
* Destination IP and port (your server)
* Process information (`python`, PID, container)
* Timing (request start, duration)
* Status code

**Object Data (sensitive):**

* HTTP method, path, query parameters
* Request headers (including custom headers)
* Request body (JSON, form data, etc.)
* Response headers
* Response body

### Direction Field Explained

* **`INGRESS` (incoming)**: Traffic coming INTO your application
  * Client → Your Python server
  * Shows what clients are requesting
* **`EGRESS` (outgoing)**: Traffic going OUT from your application
  * Your Python server → External API
  * Shows what your server is calling

***

## Troubleshooting

### Not Seeing Any Traffic?

**Check 1: Is your Python server listening on `0.0.0.0`?**

```python
# In api_server.py - must bind to 0.0.0.0, not 127.0.0.1
uvicorn.run(app, host="0.0.0.0", port=8000)
```

**Check 2: Is Qtap running BEFORE you made requests?**

```bash
# Qtap must be running first, then generate traffic
docker logs qtap-ingress  # Check if Qtap started successfully
```

**Check 3: Is your Python server actually running?**

```bash
# Test with your network IP (NOT localhost!)
curl http://192.168.1.100:8000/  # Replace with your actual IP
# Should return JSON response
```

**Check 4: Can clients reach your server?**

```bash
# Check firewall status
sudo ufw status

# If active, ensure port 8000 is allowed
sudo ufw allow 8000/tcp

# Test network connectivity from client
ping 192.168.1.100  # Replace with your server's IP
```

**Check 5: Check Qtap is hooking into Python correctly**

```bash
docker logs qtap-ingress 2>&1 | grep -i python
# Should see logs about attaching to Python process
```

### Seeing `"l7Protocol": "other"` instead of `"http1"`?

This means Qtap captured the connection but couldn't parse HTTP. Possible causes:

* Python server using HTTPS (TLS) - Qtap should still see it via SSL hooks
* Traffic is not HTTP
* Qtap not fully initialized when request was made (wait 6+ seconds)

### Too Much Noise from System Traffic?

Add process filters to exclude specific executables:

```yaml
tap:
  direction: ingress
  ignore_loopback: true

  filters:
    custom:
      - exe: /usr/sbin/sshd     # Exclude SSH daemon
        strategy: exact
      - exe: /lib/systemd/      # Exclude systemd processes
        strategy: prefix
```

***

## Real-World Example: API Gateway Monitoring

Here's a practical example for monitoring an API gateway:

```yaml
version: 2

services:
  event_stores:
    - type: stdout
  object_stores:
    - type: s3  # Store sensitive request/response data securely
      config:
        endpoint: https://minio.internal.company.com
        bucket: api-gateway-audit
        # credentials from environment

stacks:
  # Stack for all ingress traffic
  api_monitoring:
    plugins:
      - type: http_capture
        config:
          level: none      # Default: don't capture
          format: json
          rules:
            # Capture all authentication requests
            - name: "Auth endpoints"
              expr: http.req.path matches /^\/api\/auth\//
              level: full

            # Capture all errors
            - name: "Error responses"
              expr: http.res.status >= 400
              level: full

            # Capture slow requests (for performance debugging)
            - name: "Slow requests"
              expr: http.res.duration_ms > 500
              level: headers  # Headers only, no bodies

tap:
  direction: ingress
  ignore_loopback: true    # True for network traffic (production)
  http:
    stack: api_monitoring
```

***

## Next Steps

**Learn More About Ingress Capture:**

* [Traffic Capture Settings](https://docs.qpoint.io/getting-started/qtap/configuration/traffic-capture-settings) - Complete `tap` section reference
* [Traffic Processing with Plugins](https://docs.qpoint.io/getting-started/qtap/configuration/traffic-processing-with-plugins) - All plugin options
* [Complete Guide](https://docs.qpoint.io/guides/qtap-guides/getting-started/getting-started-complete-guide) - Progressive tutorial covering all Qtap features

**Production Deployment:**

* [Storage Configuration](https://docs.qpoint.io/getting-started/qtap/configuration/storage-configuration) - S3 setup for production
* [Kubernetes Manifest](https://docs.qpoint.io/getting-started/qtap/installation/kubernetes-manifest) - Deploy Qtap in K8s
* [Helm Chart](https://docs.qpoint.io/getting-started/qtap/installation/helm-chart) - Helm deployment

**Alternative: Cloud Management:**

* [Qplane](https://docs.qpoint.io/getting-started/qplane) - Manage Qtap with visual dashboards
* [POC Kick Off Guide](https://docs.qpoint.io/guides/qplane-guides/poc-kick-off-guide) - Quick start with cloud control plane

***

## Cleanup

```bash
# Stop Qtap
docker rm -f qtap-ingress

# Stop Python server (CTRL+C in the terminal)

# Remove config file
rm qtap-ingress.yaml api_server.py
```

***

*This guide uses validated configurations. All examples are tested and guaranteed to work.*
