During a red team exercise it's common to set up a relaying infrastructure to separate your external facing footprint from the actual command and control backend. Some of the popular light-weight options are to set up either HAProxy or NGINX on disposable cloud infrastructure and proxy traffic to an internal and/or secured host where an operator has access to the command and control framework. Even though this setup works in most cases, metadata such as the original source IP of a beacon is lost.


The following scenario is a very minimalistic/basic example of a command and control infrastructure and the IP adresses displayed are only figurative.

The primary command and control backend is running on a server with address The operator uses this server to directly interact with the clients/agents running a beacon.

The operator sets up a secondary server on a disposable infrastructure ( and installs HAproxy or NGINX as a proxy/forwarder. The proxy configuration will forward all traffic back to the command and control backend whenever a beacon connects. When generating a new payload, the operator makes sure that the beacons will communicate to the IP/Domain of the relay server ( and not directly with the actual command and control server itself.

From the forwarding's server perspective, the beacons connect from as the original NAT'ed source. When traffic is forwarded to the teamserver, the source address is changed and it will appear as if  the connection originates from the relay server. When using C2 backends such as cobalt strike, this will result in 'wrongfully' displaying the beacon's source IP and show the relay's IP instead.


To correctly display the original source address of the beacon we can slightly adjust the forwarding setup using a secondary HAproxy service on the same environment that is running the command and control backend.

HAproxy supports the PROXY protocol (https://www.haproxy.com/blog/haproxy/proxy-protocol/) to relay traffic without losing the original source address. In order to make this work the receiving service has to support the protocol standards. Unfortunately applications such cobalt strike don't accept PROXY sessions by default. However, we can configure a secondary HAProxy server to accept the PROXY session and forward traffic to your command and control backend with a spoofed source IP address. But wait, spoofed IP addresses via TCP?!

The secondary HAproxy instance will spoof the source IP of  to make it look like a direct connection between the beacon/agent is set up with your backend server. The HAproxy server will retain the routing metrics and information to properly reroute traffic back to the agent via the relay when processed.

In order to make this work I created a Docker template that deploys HAproxy and cobalt strike inside a separate Docker network. The cobalt strike container will use the secondary HAproxy instance as the default gateway. This makes sure that the spoofed source IP will always be routed back to the secondary HAproxy instance. And since HAproxy keeps track of all forwarded traffic, cobalt strike's response will be sent back to the original relay and back to the beacon/agent.


The repository can be found at https://github.com/d3vzer0/cnc-relay. The repository consists of two different docker compose files.

  • backend/docker-compose.yml
  • relay/docker-compose.yml


The relay docker-compose script will run a single HAproxy instance that acts as the first forwarder that a beacon communicates with. The configuration is fairly simple, but the most important line in the haproxy.cfg file is:

server proxy_server1 ${PROXYIP}:${PROXYPORT} check send-proxy

This will initiate a PROXY tunnel with the secondary HAproxy instance running on your C2 backend. You can set the destination IP and port by adusting the docker-compose.yml file and change the following variables:

  PROXYIP: "" # IP of C2 backend / secondary HAproxy
  PROXYPORT: 8080 # Port that HAproxy on C2 environment is listening on
  - "80:80" # What port should the relay listen on. Only required to change the first port

When configured, run docker-compose up -d to start HAproxy to and forward traffic to your secondary HAproxy instance via a PROXY tunnel. All traffic received on port 80 will be forwarded to Next we will set up the backend service


The backend docker-compose script will configure a custom HAproxy container by downloading the source code and compiling the application with TPROX support. The official HAproxy image doesn't come with this option enabled unfortunately. A script is included to automatically create the required IPtables and routing entries to properly forward traffic with the spoofed source address.

To make TPROX work inside the container, make sure that the host machine has the TPROX kernel module enabled. This can be done via:

modprobe xt_TPROXY && echo xt_TPROXY >> /etc/modules

The teamserver's Dockerfile is based on the official OpenJDK Docker image.   When the container boots the default gateway is removed and replaced with the IP address of the previously built HAproxy PROXY service. This makes sure that Cobalt Strike will forward the spoofed beacon IP address back to HAproxy. By default the HAproxy service will listen on port 8080 to accept the tunnel session from your relay and forward it to the specified listener port in the docker-compose file. The secondary HAproxy port 9090 is used to forward your administrative cobalt strike session with the teamserver running on port 50050.

Change the following variables in the docker-compose.yml file:

# teamserver settings
  TS_IP: "" # IP adress of the first forwarder/relay
  TS_PASSWORD: "TEAMSERVER_PASSWORD" # Teamserver password
   - "/opt/your_cs_dir:/opt/cobaltstrike" # Change to your install dir of teamserver

# haproxy settings
  TSIP: "teamserver" # dont replace, automatically resolves to your teamserver
  TSPORT: 80 # Port of cobalt strike listener, change to whatever listener you create via the teamserver
  ADMINPORT: 50050 # Port of teamserver admin service

When changed, execute docker-compose up -d to start the teamserver and secondary HAproxy gateway instance. The entire setup should now be complete. When creating and executing a new payload, the beacon's real external IP should now be visible on Cobalt Strike :) You can connect to the teamserver's admin interface by accessing port 9090 of your C2 backend IP.

Command and Control backends

In this article I used cobalt strike as a generic example for the command and control backend. Other similar projects exists that are actively being developed. These are a few examples that you can use during your red team campaign:

Empire: https://www.powershellempire.com
Cobalt Strike: https://www.cobaltstrike.com
Pupy Python: https://github.com/n1nj4sec/pupy
Meterpeter: https://www.offensive-security.com/metasploit-unleashed/about-meterpreter/