Documentation Index Fetch the complete documentation index at: https://mintlify.com/GZTimeWalker/GZCTF/llms.txt
Use this file to discover all available pages before exploring further.
GZCTF provides advanced traffic monitoring capabilities through its platform proxy system. This allows you to capture and analyze network traffic between participants and challenge containers, useful for debugging, monitoring, and post-competition analysis.
Architecture Overview
The traffic capture system consists of three main components:
ProxyController - WebSocket proxy endpoint that bridges TCP connections
RecordableNetworkStream - Wrapper that captures traffic to storage
PCAP Writer - Generates standard PCAP files for analysis tools
Configuration
Enable platform proxy in appsettings.json:
{
"ContainerProvider" : {
"PortMappingType" : "PlatformProxy" ,
"EnableTrafficCapture" : true ,
"PublicEntry" : "ctf.example.com"
}
}
PortMappingType options:
Default - Direct port mapping (Docker) or NodePort (K8s)
PlatformProxy - WebSocket proxy through GZCTF
Randomize - Direct mapping with randomized ports
Reference: /src/GZCTF/Controllers/ProxyController.cs:50-53
Proxy Endpoints
Standard Proxy (with game instance)
GET /api/proxy/{containerId}
Upgrade: websocket
Used for challenge containers associated with game instances. Includes traffic capture if enabled. Features :
Connection limiting (max 32 concurrent connections)
Traffic capture with metadata
Container validation caching
Automatic timeout (30 minutes)
Reference: /src/GZCTF/Controllers/ProxyController.cs:56-116
GET /api/proxy/NoInst/{containerId}
Upgrade: websocket
Used for admin test containers. No traffic capture. Restrictions :
Admin access only
No game instance association required
No connection limits
No traffic recording
Reference: /src/GZCTF/Controllers/ProxyController.cs:119-163
Connection Flow
Container Validation
Before establishing a proxy connection, the controller validates:
private async Task < bool > ValidateContainer ( Guid id , CancellationToken token )
{
var key = CacheKey . ConnectionCount ( id );
var bytes = await cache . GetAsync ( key , token );
// Check cache first (avoid DoS via repeated validation)
if ( bytes is not null )
return BitConverter . ToInt32 ( bytes ) >= 0 ; // -1 = invalid
// Validate in database
var valid = await containerRepository . ValidateContainer ( id , token );
// Cache result: 0 = valid, -1 = invalid
await cache . SetAsync (
key ,
BitConverter . GetBytes ( valid ? 0 : - 1 ),
ValidOption , // 10 min TTL
token
);
return valid ;
}
Invalid containers are cached as -1 to prevent repeated database queries during attacks.
Reference: /src/GZCTF/Controllers/ProxyController.cs:316-330
Connection Limiting
Each container has a connection limit to prevent abuse:
private const uint ConnectionLimit = 32 ;
private async Task < bool > IncreaseConnectionCount ( string key )
{
var bytes = await cache . GetAsync ( key );
if ( bytes is null ) return false ;
var count = BitConverter . ToInt32 ( bytes );
if ( count > ConnectionLimit ) return false ;
// Atomic increment via cache update
await cache . SetAsync (
key ,
BitConverter . GetBytes ( count + 1 ),
StoreOption // 10 hour sliding expiration
);
return true ;
}
Connections are tracked via distributed cache to work across multiple GZCTF instances.
Reference: /src/GZCTF/Controllers/ProxyController.cs:337-352
TCP Socket Creation
TCP Connection Establishment
using var socket = new Socket (
target . AddressFamily ,
SocketType . Stream ,
ProtocolType . Tcp
);
await socket . ConnectAsync ( target , token );
if ( ! socket . Connected )
throw new SocketException (( int ) SocketError . NotConnected );
// Wrap socket in recording stream
var stream = new RecordableNetworkStream (
socket ,
metadata , // Container metadata (JSON)
storage , // Blob storage provider
new RecordableNetworkStreamOptions
{
Source = clientEndpoint ,
Dest = containerEndpoint ,
EnableCapture = enableTrafficCapture ,
BlobPath = container . TrafficPath ( connectionId )
}
);
Reference: /src/GZCTF/Controllers/ProxyController.cs:168-181
WebSocket Proxy
Bidirectional Forwarding
The proxy runs two concurrent tasks for bidirectional forwarding:
private static async Task <( ulong , ulong )> RunProxy (
RecordableNetworkStream stream ,
WebSocket ws ,
CancellationToken token )
{
using var cts = CancellationTokenSource . CreateLinkedTokenSource ( token );
cts . CancelAfter ( TimeSpan . FromMinutes ( 30 )); // 30-minute timeout
ulong tx = 0 , rx = 0 ; // Byte counters
// WebSocket → TCP (sender task)
var sender = Task . Run ( async () =>
{
var buffer = ArrayPool < byte >. Shared . Rent ( 4096 );
try
{
while ( true )
{
var status = await ws . ReceiveAsync ( buffer , ct );
if ( status . CloseStatus . HasValue ) break ;
if ( status . Count <= 0 ) continue ;
tx += ( ulong ) status . Count ;
await stream . WriteAsync (
buffer . AsMemory ( 0 , status . Count ),
ct
);
}
}
finally
{
ArrayPool < byte >. Shared . Return ( buffer );
}
}, ct );
// TCP → WebSocket (receiver task)
var receiver = Task . Run ( async () =>
{
var buffer = ArrayPool < byte >. Shared . Rent ( 4096 );
try
{
while ( true )
{
var count = await stream . ReadAsync ( buffer , ct );
if ( count == 0 )
{
await ws . CloseAsync (
WebSocketCloseStatus . Empty ,
null ,
ct
);
break ;
}
rx += ( ulong ) count ;
await ws . SendAsync (
buffer . AsMemory ( 0 , count ),
WebSocketMessageType . Binary ,
endOfMessage : true ,
ct
);
}
}
finally
{
ArrayPool < byte >. Shared . Return ( buffer );
}
}, ct );
// Wait for either task to complete
await Task . WhenAny ( sender , receiver );
await cts . CancelAsync ();
await Task . WhenAll ( sender , receiver );
return ( tx , rx ); // Return traffic stats
}
Key Features :
4KB buffer size for optimal throughput
ArrayPool for reduced allocations
30-minute connection timeout
Binary WebSocket frames
Graceful close on TCP disconnect
Reference: /src/GZCTF/Controllers/ProxyController.cs:229-308
Traffic Capture
RecordableNetworkStream
The RecordableNetworkStream wraps a TCP socket and captures all traffic:
public class RecordableNetworkStreamOptions
{
public IPEndPoint Source { get ; set ; } // Client IP:port
public IPEndPoint Dest { get ; set ; } // Container IP:port
public bool EnableCapture { get ; set ; } // Enable recording
public string BlobPath { get ; set ; } // Storage path
}
Reference: /src/GZCTF/Controllers/ProxyController.cs:109-115
Each PCAP file starts with JSON metadata:
var metadata = container . GenerateMetadata ( JsonOptions );
// Metadata includes:
{
"containerName" : "web_challenge_abc123" ,
"challengeId" : 42 ,
"challengeName" : "SQL Injection 101" ,
"teamId" : "team_123" ,
"teamName" : "HackTheBox" ,
"userId" : "user_456" ,
"userName" : "Alice" ,
"startedAt" : "2024-03-01T12:00:00Z" ,
"containerIp" : "172.17.0.5" ,
"containerPort" : 80
}
Metadata is written as the first “packet” in the PCAP for context.
Reference: /src/GZCTF/Controllers/ProxyController.cs:103-104
Traffic is captured in standard PCAP format compatible with Wireshark:
PCAP File Structure:
[Global Header - 24 bytes]
- Magic Number: 0xa1b2c3d4
- Version: 2.4
- Timezone: UTC
- Timestamp accuracy: microseconds
- Snapshot length: 65535
- Link type: Ethernet (1)
[Metadata Packet]
- Timestamp: Connection start
- Length: JSON metadata size
- Data: UTF-8 JSON
[Packet 1]
- Timestamp: Packet capture time
- Original length
- Captured length
- Ethernet + IP + TCP headers
- Payload data
[Packet 2]
...
Storage Path Convention
public string TrafficPath ( string connectionId )
{
return $"capture/ { gameId } / { challengeId } / { containerId } / { connectionId } .pcap" ;
}
Example: capture/42/101/abc123-def456/conn_789.pcap
Traffic Analysis
Downloading Captures
Admins can download PCAP files via the game monitor:
GET / api / game / { gameId } / challenges / { challengeId } / traffic
Response :
[
{
"name" : "web_challenge_abc123" ,
"size" : 524288 ,
"category" : "Web" ,
"count" : 15 , // Number of captures
"files" : [
{
"name" : "conn_789.pcap" ,
"size" : 35000 ,
"team" : "HackTheBox" ,
"timestamp" : "2024-03-01T12:30:00Z"
}
]
}
]
Wireshark Analysis
PCAP files can be opened directly in Wireshark:
# Download PCAP
curl -H "Authorization: Bearer $TOKEN " \
https://ctf.example.com/api/game/42/traffic/conn_789.pcap \
-o capture.pcap
# Open in Wireshark
wireshark capture.pcap
# Or use tcpdump
tcpdump -r capture.pcap -A
Filtering Traffic
Wireshark display filters for common analysis:
# Show only HTTP requests
http.request
# Find flag submissions
frame contains "flag{"
# Filter by team IP
ip.src == 172.17.0.5
# Show TCP retransmissions (connection issues)
tcp.analysis.retransmission
# Find SQL injection attempts
frame contains "UNION SELECT"
Security Considerations
Privacy Notice : Traffic capture records all data transmitted between players and containers. Ensure:
Players are informed about traffic monitoring in competition rules
PCAP files are stored securely with access controls
Captures are deleted after a retention period
Sensitive data (credentials, tokens) in captures are protected
Connection Limits
Default limits prevent abuse:
32 concurrent connections per container
30-minute timeout per connection
10-minute validation cache to prevent DoS
Adjust in ProxyController.cs if needed:
private const uint ConnectionLimit = 32 ;
private const int TimeoutMinutes = 30 ;
IP Address Handling
Client IPs are captured for traffic analysis:
var clientIp = HttpContext . Connection . RemoteIpAddress ;
var clientPort = HttpContext . Connection . RemotePort ;
if ( clientIp is null )
return BadRequest ( new RequestResponse (
localizer [ nameof ( Resources . Program . Container_InvalidClientAddress )]
));
IPv6 addresses are normalized to IPv4 when possible.
Reference: /src/GZCTF/Controllers/ProxyController.cs:95-99
Storage Integration
Traffic captures work with all GZCTF storage backends:
Local Storage Stores PCAP files in files/capture/ Best for testing and small competitions.
S3/MinIO Stores captures in S3-compatible object storage. Recommended for production with automatic cleanup.
Azure Blob Stores in Azure Blob Storage containers. Integrates with Azure ecosystem.
See Integrations for storage configuration.
Buffer Tuning
Default buffer size is 4KB. Increase for high-throughput scenarios:
private const int BufferSize = 4096 ; // Increase to 8192 or 16384
Connection Pooling
The proxy reuses WebSocket connections efficiently:
Uses ArrayPool<byte> to reduce GC pressure
Async I/O prevents thread blocking
Cancellation tokens enable clean shutdown
Capture Overhead
Traffic capture adds minimal overhead:
Async writes to storage (non-blocking)
Buffered I/O reduces syscalls
No packet parsing (raw capture)
Typical overhead: < 5% latency increase
Debugging Proxy Issues
Enable Debug Logging
{
"Logging" : {
"LogLevel" : {
"GZCTF.Controllers.ProxyController" : "Debug"
}
}
}
Common Issues
Symptoms : SocketException: Connection refusedCauses :
Container not running or not ready
Wrong port configuration
Network policy blocking traffic
Solution :# Check container status
docker ps | grep {container_id}
# Test TCP connectivity
nc -zv {container_ip} {port}
Symptoms : HTTP 400/403 on WebSocket upgradeCauses :
Platform proxy disabled
Container validation failed
Connection limit reached
Solution : Check proxy configuration and container status
Traffic Capture Not Working
Symptoms : PCAP files not createdCauses :
EnableTrafficCapture = false
Storage backend not configured
Insufficient storage permissions
Solution :{
"ContainerProvider" : {
"EnableTrafficCapture" : true
}
}
Client-Side Integration
Players connect via WebSocket from their browser:
const ws = new WebSocket (
`wss://ctf.example.com/api/proxy/ ${ containerId } `
);
ws . binaryType = 'arraybuffer' ;
ws . onopen = () => {
console . log ( 'Connected to challenge' );
};
ws . onmessage = ( event ) => {
// Receive TCP data from container
const data = new Uint8Array ( event . data );
console . log ( 'Received:' , data );
};
ws . send ( new TextEncoder (). encode ( 'nc command \n ' ));
GZCTF provides WebSocket-based terminal emulators for challenges requiring interactive shells.
Next Steps
Container Providers Configure Docker/K8s for platform proxy mode
Integrations Set up external storage for traffic captures