How to Mirror Live TCP Traffic to Multiple Servers for Benchmarking in Debian


1 views

When maintaining production web servers, we often face a dilemma: how to test configuration changes without impacting live customer traffic. The ideal solution would be real-time traffic mirroring that sends identical requests to both production and test servers simultaneously.

After extensive testing, I found most common approaches had limitations:

  • IPTables TEE only works for local network destinations
  • Proxy solutions typically don't maintain dual forwarding
  • Simple port forwarding tools lose connection state

Here's a robust Python implementation that handles multiple ports and maintains proper socket connections:

import socket
import sys
import thread
import time

class TrafficMirror:
    def __init__(self, config_file):
        self.config = self._parse_config(config_file)
        
    def _parse_config(self, config_file):
        """Parse config file with format: local_port local_target_port remote_ip remote_port"""
        configs = []
        with open(config_file) as f:
            for line in f:
                if line.strip() and not line.startswith('#'):
                    parts = line.split()
                    configs.append({
                        'listen_port': int(parts[0]),
                        'local_port': int(parts[1]),
                        'remote_ip': parts[2],
                        'remote_port': int(parts[3])
                    })
        return configs
    
    def _handle_connection(self, client_sock, config):
        try:
            # Receive client data
            data = client_sock.recv(4096)
            
            if not data:
                return
            
            # Forward to local server
            local_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            local_sock.connect(('127.0.0.1', config['local_port']))
            local_sock.sendall(data)
            
            # Get response from local
            local_response = local_sock.recv(4096)
            local_sock.close()
            
            # Send response to client
            client_sock.sendall(local_response)
            client_sock.close()
            
            # Mirror to remote (async to avoid latency impact)
            thread.start_new_thread(
                self._mirror_to_remote,
                (data, config['remote_ip'], config['remote_port'])
            )
            
        except Exception as e:
            sys.stderr.write(f"Error handling connection: {str(e)}\n")
    
    def _mirror_to_remote(self, data, remote_ip, remote_port):
        try:
            remote_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            remote_sock.connect((remote_ip, remote_port))
            remote_sock.sendall(data)
            remote_sock.close()
        except Exception as e:
            sys.stderr.write(f"Mirroring failed: {str(e)}\n")
    
    def start(self):
        for config in self.config:
            thread.start_new_thread(self._create_listener, (config,))
        
        # Keep main thread alive
        while True:
            time.sleep(60)
    
    def _create_listener(self, config):
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        sock.bind(('0.0.0.0', config['listen_port']))
        sock.listen(5)
        
        while True:
            client_sock, addr = sock.accept()
            thread.start_new_thread(
                self._handle_connection,
                (client_sock, config)
            )

if __name__ == '__main__':
    mirror = TrafficMirror('mirror_config.cfg')
    mirror.start()

Create a mirror_config.cfg file:

# Format: listen_port local_port remote_ip remote_port
80 8080 192.168.1.100 80
443 8443 192.168.1.100 443

For enterprise deployments, consider these enhancements:

  • Add connection pooling for remote mirrors
  • Implement request buffering for high traffic volumes
  • Add health checks for mirror destinations
  • Include detailed logging and metrics

For those preferring non-Python solutions:

  1. HAProxy with mirroring (version 1.8+):
    frontend http-in
        bind *:80
        mode http
        use_backend primary
        mirror backend test
  2. Nginx with mirror module:
    location / {
        mirror /mirror;
        proxy_pass http://primary;
    }
    
    location = /mirror {
        internal;
        proxy_pass http://test$request_uri;
    }

When working with production web servers handling customer traffic, testing configuration changes or new code versions becomes particularly challenging. The need arises to duplicate incoming HTTP requests to both:

  • The existing production server (Apache 2.2.16 in this case)
  • One or more test servers for benchmarking purposes

This becomes especially complex when dealing with multiple ports (60+ in this scenario) beyond the standard 80/443.

Several approaches have been attempted without success:

- iptables TEE: Limited to local network only
- agnoster duplicator: Requires one session per port (not scalable)
- kklis proxy: Forwards only (no local processing)
- socat: TEE function writes to filesystem only

The following Python script provides a working solution that:

  1. Listens on specified ports
  2. Forwards traffic to local Apache instance
  3. Mirrors traffic to remote servers
  4. Handles responses from local server
import socket
import SimpleHTTPServer
import SocketServer
import sys, thread, time

def main(config, errorlog):
    sys.stderr = file(errorlog, 'a')
    for settings in parse(config):
        thread.start_new_thread(server, settings)
    while True:
        time.sleep(60)

def parse(configline):
    settings = list()
    for line in file(configline):
        parts = line.split()
        settings.append((int(parts[0]), int(parts[1]), parts[2], int(parts[3])))
    return settings

def server(*settings):
    try:
        dock_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        dock_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        dock_socket.bind(('', settings[0]))
        dock_socket.listen(5)

        while True:
            client_socket = dock_socket.accept()[0]
            client_data = client_socket.recv(1024)
            
            # Forward to local
            local_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            local_socket.connect(('', settings[1]))
            local_socket.sendall(client_data)
            
            # Get response
            client_response = local_socket.recv(1024)
            local_socket.close()
            
            # Respond to client
            client_socket.sendall(client_response)
            client_socket.close()
            
            # Mirror to remote
            remote_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            remote_socket.connect((settings[2], settings[3]))
            remote_socket.sendall(client_data)       
            remote_socket.close()
            
    except:
        print "[ERROR]: ", sys.exc_info()
        raise

if __name__ == '__main__':
    main('multiforwarder.config', 'error.log')

Create a configuration file multiforwarder.config with entries in the format:

8080 80 remote1.example.com 80
8443 443 remote2.example.com 443

Where each line specifies:

listen_port local_forward_port remote_ip remote_port

For cases where real-time mirroring isn't strictly necessary, consider:

# Capture traffic
tcpdump -i eth0 -w capture.pcap port 80 or port 443

# Replay to test servers
tcpreplay -i eth1 -K --loop=5 capture.pcap

When implementing this solution:

  • Monitor CPU and memory usage
  • Consider implementing queueing for high traffic volumes
  • Ensure proper error handling for network interruptions
  • Test with increasing load to identify bottlenecks