Skip to content
 
 

Repository files navigation

CnCNet Tunnel Server

A high-performance UDP relay server for Command & Conquer games on CnCNet, supporting V2/V3 tunnel protocols, STUN-based P2P NAT traversal, DDoS protection and a real-time web dashboard.


Table of Contents

  1. Ports
  2. Docker
  3. Linux (systemd)
  4. Windows
  5. Matchmaking Server
  6. Configuration Reference
  7. Country Lookup
  8. Web Dashboard

Ports

Open these on your firewall before starting the server.

Port Protocol Purpose
50001 UDP V3 Tunnel
50000 TCP+UDP V2 Tunnel
8054 UDP STUN Server
3478 UDP STUN Server
1337 TCP Web Dashboard
50002 UDP V3 Tunnel — matchmaking (see below)
1338 TCP Web Dashboard — matchmaking

Docker

The recommended setup. Requires Docker with the Compose plugin.

1. Create a working directory and download the config template:

mkdir -p /opt/cncnet && cd /opt/cncnet
curl -O https://github.com/ghraw/Rowtag/cncnet-server/master/appsettings.json
curl -O https://github.com/ghraw/Rowtag/cncnet-server/master/docker-compose.yml

2. Edit the config — set at minimum Server.Name and Maintenance.Password:

nano appsettings.json

3. Start:

docker compose up -d

Useful commands:

# View logs
docker compose logs -f

# Restart after config change
docker compose restart

# Update to latest image
docker compose pull && docker compose up -d

# Stop
docker compose down

Firewall (UFW):

ufw allow 50000/tcp
ufw allow 50000/udp
ufw allow 50001/udp
ufw allow 3478/udp
ufw allow 8054/udp
ufw allow 1337/tcp

Note: If using UFW with Docker, ensure /etc/default/ufw has DEFAULT_FORWARD_POLICY="ACCEPT" and restart Docker after any UFW reload: systemctl restart docker

UFW does not restrict published Docker ports. Docker inserts its own rules ahead of UFW, so the ports above are reachable from anywhere the moment the container starts, and a rule like ufw deny 1337/tcp or ufw allow from <ip> to any port 1337 has no effect on them. To actually limit access — the dashboard on 1337 above all — write the rule into the DOCKER-USER chain instead:

iptables -I DOCKER-USER -p tcp --dport 1337 -s <your-ip> -j ACCEPT
iptables -I DOCKER-USER -p tcp --dport 1337 -j DROP

These rules are lost on reboot and when Docker restarts, so persist them with a small systemd unit. Always verify from a host that is not on the allow list, otherwise you are testing nothing.


Linux (systemd)

Requires .NET Runtime 10.

1. Install .NET runtime:

sudo apt-get update && sudo apt-get install -y dotnet-runtime-10.0

2. Create a dedicated user and install the binary:

sudo useradd -m -r cncnet-server

# Download and extract the latest release (adjust filename for your architecture)
sudo unzip cncnet-server-*-linux-x64.zip -d /opt/cncnet-server
sudo chown -R cncnet-server:cncnet-server /opt/cncnet-server
sudo chmod +x /opt/cncnet-server/cncnet-server

3. Edit the config:

sudo nano /opt/cncnet-server/appsettings.json

4. Create the systemd service — save as /etc/systemd/system/cncnet-server.service:

[Unit]
Description=CnCNet Tunnel Server
After=network.target

[Service]
Type=simple
User=cncnet-server
WorkingDirectory=/opt/cncnet-server
ExecStart=/opt/cncnet-server/cncnet-server
Restart=always
RestartSec=5
KillSignal=SIGINT

[Install]
WantedBy=multi-user.target

5. Enable and start:

sudo systemctl daemon-reload
sudo systemctl enable --now cncnet-server

# Follow logs
sudo journalctl -u cncnet-server -f

6. Open firewall ports:

sudo ufw allow 50000/tcp
sudo ufw allow 50000/udp
sudo ufw allow 50001/udp
sudo ufw allow 3478/udp
sudo ufw allow 8054/udp
sudo ufw allow 1337/tcp

Updating:

sudo systemctl stop cncnet-server
sudo unzip -o cncnet-server-*-linux-x64.zip -d /opt/cncnet-server
sudo systemctl start cncnet-server

Windows

Requires .NET Runtime 10.

1. Extract the release archive:

Expand-Archive cncnet-server-*-win-x64.zip -DestinationPath C:\cncnet-server

2. Edit the config:

notepad C:\cncnet-server\appsettings.json

3. Install and start as a Windows Service:

New-Service -Name CnCNetServer `
  -BinaryPathName '"C:\cncnet-server\cncnet-server.exe"' `
  -StartupType Automatic `
  -DisplayName "CnCNet Tunnel Server"

Start-Service CnCNetServer

Useful commands:

# View status
Get-Service CnCNetServer

# Stop / start
Stop-Service CnCNetServer
Start-Service CnCNetServer

# Remove service
Remove-Service CnCNetServer

Updating:

Stop-Service CnCNetServer
Expand-Archive -Force cncnet-server-*-win-x64.zip -DestinationPath C:\cncnet-server
Start-Service CnCNetServer

Firewall:

New-NetFirewallRule -DisplayName "CnCNet" -Direction Inbound -Action Allow `
  -Protocol TCP -LocalPort 50000,1337
New-NetFirewallRule -DisplayName "CnCNet UDP" -Direction Inbound -Action Allow `
  -Protocol UDP -LocalPort 50000,50001,3478,8054

Matchmaking Server

A matchmaking server is where two clients meet to swap tunnel lists and agree which relay tunnels to test, before either registers on a relay. That exchange is a few small packets lasting seconds, so one matchmaking server holds far more clients than a relay — which is the point: it stops every client having to register on every tunnel just to find a good one.

It does not carry game traffic. It relays only the negotiation exchange and drops everything else. It announces itself to the master list as version 4, so clients released before matchmaking existed ignore it entirely and can never pick it to host a game.

Matchmaking is a mode of the V3 tunnel, and a process has one V3 listener — so a server that does both roles runs the binary twice, from two directories, with two configs.

Docker

docker-compose.matchmaking.yml in this repository is the second instance, ready to run. Put it in its own directory next to your game tunnel:

mkdir -p /opt/cncnet-matchmaking && cd /opt/cncnet-matchmaking
curl -O https://github.com/ghraw/Rowtag/cncnet-server/master/docker-compose.matchmaking.yml
curl -o .env https://github.com/ghraw/Rowtag/cncnet-server/master/.env.matchmaking.example
nano .env && chmod 600 .env
docker compose -f docker-compose.matchmaking.yml up -d

The .env carries the server name and both passwords, so nothing secret sits in the compose file. GEO_DB points at the country database the game tunnel already uses — the two instances share one file. Everything else is already set: V3 on 50002, V2 and STUN off so they cannot collide with the first instance, the dashboard on 1338, and mem_limit/cpu_shares low enough that the game tunnel wins when both want the machine.

Open UDP 50002, and TCP 1338 only if you want the second dashboard reachable — mind the note in the firewall section, published Docker ports ignore UFW. Then confirm the role took:

docker logs cncnet-matchmaking | grep "matchmaking mode"
# V3 Tunnel started on UDP port 50002 in matchmaking mode (max 2000 clients, 30s timeout, 32 per IP)

If that line says anything else, the process came up as a plain relay and the master list will advertise it as one.

Bing

Not using Docker? The same thing by hand, in three steps. Copy your existing install to a second directory:

cp -r /opt/cncnet-server /opt/cncnet-server-matchmaking

Bang

Create /opt/cncnet-server-matchmaking/appsettings.local.json:

{
  "Server":       { "Name": "Your Server Name (Matchmaking)" },
  "TunnelV3":     { "Enabled": true, "Port": 50002,
                    "Matchmaking": { "Enabled": true, "MaxClients": 2000, "ClientTimeout": 25, "IpLimit": 32 } },
  "TunnelV2":     { "Enabled": false },
  "PeerToPeer":   { "Enabled": false },
  "MasterServer": { "Enabled": true },
  "WebMonitor":   { "Enabled": true, "Port": 1338 },
  "Logging":      { "LogDirectory": "logs-matchmaking" },
  "Maintenance":  { "Password": "your-dashboard-password" }
}

Four of those exist to avoid colliding with the first instance, and skipping any will bite you:

Setting Why
PeerToPeer: false The first instance already holds STUN 8054/3478; binding them again kills startup.
WebMonitor.Port: 1338 Same, for the dashboard on 1337.
Logging.LogDirectory Two processes rolling the same log files fight over locks — and it fails quietly.
TunnelV2: false No point running a second V2.

Boom

sed 's/cncnet-server/cncnet-server-matchmaking/g' /etc/systemd/system/cncnet-server.service \
  > /etc/systemd/system/cncnet-server-matchmaking.service
systemctl daemon-reload
systemctl enable --now cncnet-server-matchmaking

Open UDP 50002 (and TCP 1338 if you want the dashboard), then confirm it came up in the right role:

journalctl -u cncnet-server-matchmaking -n 20 | grep "V3 Tunnel started"
# V3 Tunnel started on UDP port 50002 in matchmaking mode (max 2000 clients, 25s timeout, 32 per IP)

Within a minute it should appear in https://cncnet.org/master-list with a trailing version field of 4. If it doesn't, check the log for heartbeat failed.

Only a handful of servers should run matchmaking. Every client contacts all of them on every lobby join, so the set is meant to stay small and stable.


Configuration Reference

All settings are read from appsettings.json in the working directory.

Priority order (highest wins): CLI arguments > Environment variables (CNCNET_*) > appsettings.local.json > appsettings.json

Security tip: Never put passwords in appsettings.json. Use appsettings.local.json (excluded from version control) or environment variables instead.

{
  "Server": {
    "Name": "My CnCNet Server",
    "MaxClients": 200,
    "ClientTimeout": 60
  },
  "TunnelV3": {
    "Enabled": true,
    "Port": 50001,
    "IpLimit": 8,
    "DDoSProtectionEnabled": true
  },
  "TunnelV2": {
    "Enabled": true,
    "Port": 50000,
    "IpLimit": 4,
    "DDoSProtectionEnabled": true
  },
  "PeerToPeer": {
    "Enabled": true,
    "StunPort1": 8054,
    "StunPort2": 3478
  },
  "MasterServer": {
    "Enabled": true,
    "Url": "https://cncnet.org/master-announce",
    "Password": "",
    "AnnounceIntervalSeconds": 60
  },
  "Maintenance": {
    "Password": ""
  },
  "Security": {
    "IpBlacklistDurationHours": 24,
    "MaxPingsPerIp": 20,
    "MaxPingsGlobal": 5000,
    "ExternalBlacklistUrls": [
      "https://www.spamhaus.org/drop/drop.txt",
      "https://www.spamhaus.org/drop/edrop.txt",
      "https://github.com/ghraw/firehol/blocklist-ipsets/master/firehol_level1.netset",
      "https://www.binarydefense.com/banlist.txt"
    ]
  },
  "WebMonitor": {
    "Enabled": true,
    "Port": 1337
  },
  "Logging": {
    "LogDirectory": "logs",
    "RetentionDays": 15,
    "RollingIntervalDays": 2,
    "MinimumLevel": "Information"
  }
}

Key settings:

Setting Description
Server.Name Server name shown on the master server list
Server.MaxClients Maximum simultaneous tunnel clients
Server.ClientTimeout Seconds before an idle client is dropped
TunnelV3.IpLimit Max connections per IP on V3 (1–40)
TunnelV3.RelayPacketCopies Copies sent of each relayed packet (1–3). 1 is normal. Above 1 trades upstream bandwidth for loss resilience, and delivers duplicate datagrams to clients — see the note below
TunnelV3.Matchmaking.Enabled Run this V3 listener as a matchmaking server instead of a game relay
TunnelV3.Matchmaking.MaxClients Client limit in matchmaking mode, replacing Server.MaxClients
TunnelV3.Matchmaking.ClientTimeout Idle timeout in matchmaking mode, replacing Server.ClientTimeout
TunnelV3.Matchmaking.IpLimit Per-IP limit in matchmaking mode, replacing TunnelV3.IpLimit
TunnelV3.Matchmaking.MaxRelayPacketBytes Largest packet a matchmaking server will relay
TunnelV2.IpLimit Max connections per IP on V2 (1–40)
MasterServer.Password Password to register on the public master server
Maintenance.Password Password to access the web dashboard
Security.IpBlacklistDurationHours How long an auto-banned IP stays blocked

A note on packet duplication

RelayPacketCopies above 1 is a blunt instrument, and worth understanding before you reach for it:

  • It only protects the server-to-receiver leg. A packet lost on its way to this server is gone before duplication happens.
  • Copies go out back to back, so it survives a random single drop but not a burst — and congestion loses consecutive packets, which is exactly the case it misses.
  • It multiplies outbound game bandwidth by that factor, and clients receive duplicate datagrams. Client negotiation traffic tolerates duplicates by design; confirm the game itself does before enabling it in anger.

It can also be changed live from the dashboard without restarting.


Country Lookup (optional)

The dashboard can show the country of each connected client. The lookup runs against a local database file and only during the status poll, over the client set the tunnel already holds — there is no per-packet cost, and the raw address is never stored.

The database is not shipped with this repository. Download the free DB-IP Country Lite database (or a MaxMind GeoLite2 Country database) and place it where the server can read it:

# Docker: put it next to your docker-compose.yml and mount it
volumes:
  - ./dbip-country-lite.mmdb:/app/dbip-country-lite.mmdb:ro

# Binary install: place it next to the executable
/opt/cncnet-server/dbip-country-lite.mmdb

Without the file the server starts normally and simply reports ?? as the country. If you build the Docker image yourself, dropping the file into the build context is enough — it is picked up automatically and the build works with or without it.

DB-IP Country Lite is licensed CC-BY 4.0 and requires attribution to DB-IP wherever the data is displayed.


Web Dashboard

Accessible at http://<your-server>:1337 (port configurable via WebMonitor.Port).

Set Maintenance.Password in your config to require a login. Leave empty to disable authentication.

Features:

Feature Description
Tunnel status Connected clients, unique IPs, maintenance mode toggle
Security controls Enable/disable DDoS protection and V3 packet validation
Configuration Adjust IP limits and blacklist duration at runtime
Blocked IPs View and manually unblock entries from the local blacklist
Log viewer Last 50 log messages with level highlighting

License

GPL-3.0 — see LICENSE for details.

Links


made with love by Rowtag

About

CnCNet Tunnel Server

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages