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
First, let's create a simple FastAPI web server that we'll monitor.
Install FastAPI and Uvicorn
Create the Web Server
Save this as api_server.py:
Start the Server
In one terminal, start the Python server:
You should see:
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:
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:
Or using the Linux binary:
Step 2: Find Your Host's Network IP Address
Find the IP address of the machine running your Python server:
The IP address before the / is what you'll use (e.g., 192.168.1.100).
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.
Step 3: Allow Firewall Access (If Needed)
If you have a firewall enabled, allow traffic on port 8000:
Step 4: Generate Test Traffic from Another Host
From another machine on your network (or the same machine using its network IP):
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.
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:
Step 5: View Captured Traffic
Check the Qtap logs to see the captured ingress traffic:
What you should see:
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
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)
python api_server.py
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)
# 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
# On Ubuntu/Debian with ufw
sudo ufw allow 8000/tcp
sudo ufw status
# Or temporarily disable for testing
# sudo ufw disable
# 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": "[email protected]", "role": "admin"}'
# Test 5: Slow endpoint (tests latency tracking)
curl http://192.168.1.100:8000/api/slow
# 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
# If using Docker
docker logs qtap-ingress
# Look for output showing the Python process and incoming requests
=== 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": "[email protected]", "role": "admin"}
--- Response Headers ---
Content-Type: application/json
Content-Length: 89
--- Response Body ---
{"message":"User created successfully","data":{"name":"Alice Smith","email":"[email protected]","role":"admin"},"id":12345}
========================
tap:
direction: all # Capture everything
ignore_loopback: false # Include localhost
http:
stack: ingress_stack
stacks:
headers_only:
plugins:
- type: http_capture
config:
level: headers # (summary|headers|full) - Headers includes headers but not bodies
format: text
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
# 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)
# Qtap must be running first, then generate traffic
docker logs qtap-ingress # Check if Qtap started successfully
# Test with your network IP (NOT localhost!)
curl http://192.168.1.100:8000/ # Replace with your actual IP
# Should return JSON response
# 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
docker logs qtap-ingress 2>&1 | grep -i python
# Should see logs about attaching to Python process