Protecting a Minecraft proxy from DDoS attacks
Why reachable backend servers make every measure on the proxy useless, what separates BungeeCord ip_forward from Velocity modern forwarding, how to cap connections per source address, and from which attack size on only upstream filtering helps.
A Minecraft network with a proxy has exactly one point where everything comes together: the address your players typed in. If a lobby goes down, the people standing on it notice. If the proxy goes down, everyone is out, including the players who have been building on the survival server for four hours. This article shows how to protect a Minecraft proxy from DDoS attacks: first the backends, which almost every attacked network leaves exposed on the internet, then the forwarding secret, then the four attack types that hit a proxy together with the matching configuration blocks, and finally the point where local measures physically stop working.
Everything here applies to BungeeCord, Waterfall or Velocity in front of Paper, Spigot or Fabric backends on Debian 12, Debian 13, Ubuntu 22.04 LTS or Ubuntu 24.04 LTS. The commands are written for root, as a regular user put sudo in front. Protocol attacks against a single Java Edition server are covered in Minecraft DDoS protection and nullping protection, and the Bedrock Edition with its UDP protocol is covered in Protecting a Minecraft Bedrock server from DDoS attacks. This article deals only with what the proxy itself creates.
If the attack is running right now: do not restart the proxy. A restart throws out every player who is still connected and gives you no measurement at all. Secure the values from the section "Collect measurements before it goes wrong" first, because afterwards they are gone for good.
Why a Minecraft proxy is the most sensitive point in the network
A proxy is both the best and the worst news for DDoS protection. The best, because there is exactly one public address that every protective measure can be aimed at: one port, one protocol, one rule set. The worst, because that single address is also the only target an attacker has to hit. A Minecraft network with a proxy has no second entrance through which players could arrive once the first one is saturated.
On top of that comes the attack surface of the proxy itself. It accepts every incoming TCP connection, reads the handshake, decides between status and login, runs the Mojang authentication, opens a second connection to the backend and then forwards every packet in both directions. A backend server only does the last part of that. The proxy therefore carries the entire connection load of the network, and it carries it twice, because every player connection has a backend connection attached to it. What a DDoS attack is in the first place is explained in What is a DDoS attack?.
How a proxy network is built
A Minecraft proxy is a program that behaves like a Minecraft server towards the player and like a player towards the real servers. It speaks the same protocol on port 25565 TCP, answers the server list query, performs the login and then connects the player to one of the backend servers. When the player switches worlds, the connection to the proxy stays up and only the connection behind it is swapped out. That is exactly why players experience a server switch as nothing more than a short loading screen.
The usual layout looks like this, and the numbers are the defaults of the respective software:
Player --> play.example.com (A or SRV record in DNS)
|
v
Proxy 25565/TCP (Velocity, default bind = "0.0.0.0:25565")
Proxy 25577/TCP (BungeeCord, default listeners.host: 0.0.0.0:25577)
|
+--------+--------+------------------+
v v v
Lobby 25566 Survival 25567 Minigames 25568
127.0.0.1 127.0.0.1 10.0.0.12
Two details matter for protection. First, the Java Edition supports SRV records in DNS, so players only type play.example.com without a port number. Which address sits behind that is up to you, and it can be changed without the players doing anything. Second, the backend ports never have to be public. The proxy reaches them over 127.0.0.1, over a private network, or over an address only it knows.
The ports that actually matter
A proxy network needs exactly one open port to the outside. Everything else in this table belongs behind a firewall:
| Port | Protocol | What listens | Belongs on the open internet? |
|---|---|---|---|
| 25565 | TCP | Java Edition default port, also the Velocity default (bind = "0.0.0.0:25565") |
yes, this one and only this one |
| 25577 | TCP | BungeeCord and Waterfall default (listeners.host: 0.0.0.0:25577) |
yes, if the proxy listens there |
| 25566 to 25575 | TCP | backend servers: lobby, survival, minigames, creative | no, never |
| 25565 | UDP | GS4 query of a backend (enable-query, query.port, off by default) |
no |
| 25575 | TCP | RCON (enable-rcon, rcon.port, off by default) |
no, never |
| 25577 | UDP | proxy query (BungeeCord query_port, Velocity [query] port = 25565, both off by default) |
no |
| 19132 | UDP | Geyser, if Bedrock clients should reach the network | only then, otherwise closed |
| 8080 | TCP | Pterodactyl Wings, the interface between panel and node | only for the panel, not for the world |
| 2022 | TCP | SFTP of Pterodactyl Wings | restrict to fixed addresses |
| 22 | TCP | SSH access | restrict to fixed addresses |
The row with 25577 is the source of a stubborn misunderstanding: BungeeCord listens on 25577 out of the box, Velocity on 25565. Anyone who migrates from BungeeCord to Velocity and keeps the old firewall rule suddenly has a proxy exposed on a port they never wrote a rule for, plus an open port with nothing behind it. After the switch, check with ss -lntp what is really bound.
The most common and most expensive mistake: reachable backend servers
This is the most important section of the article, and it costs you no money, only twenty minutes. If you leave the backend servers open on the internet, the proxy was pointless. An attacker who knows the address of a backend bypasses every measure you took on the proxy: every connection throttle, every queue, every login check, every ban system.
The damage is not limited to availability either. In almost every network it is a complete loss of privileges at the same time, and the next chapter on the forwarding secret explains why.
How attackers find your backends
There are four ways, and none of them requires any particular skill.
First: port scans on 25565 to 25575. The entire IPv4 address space is continuously scanned for open Minecraft ports, and the results end up in publicly searchable directories. Anyone who assigns backends in ascending order from 25566, which is what almost everybody does, makes it especially easy: a scan across eleven ports of one known address reveals the whole topology.
Second: the status response itself. A Minecraft server answers the server list query before anyone has logged in. The answer is a JSON document with the version name, the protocol number, the current and maximum player count, the description from motd and a sample of the connected players including their UUIDs. That is exactly how a scanner recognizes that port 25567 is not a web service but a Minecraft world, and it reads along whether the attack is worth it.
Third: the old address in DNS. Many networks used to run a single server before the proxy, and the A record from back then still points at that machine. Historical DNS data is publicly available. Anyone who puts a proxy in front without changing the old address is effectively handing out the backend address.
Fourth: plugins and status pages. A Discord bot, a web interface showing the player count per world, or a query plugin that enables GS4 query on every backend publishes the addresses in a place nobody thinks about while hardening.
The firewall rules that close this
The rule in one sentence: only the address of the proxy may reach the backend ports, everything else is dropped. If the proxy runs on the same machine as the backends, this is trivial, because the backends then bind to 127.0.0.1 and are technically unreachable from outside. In the server.properties of each backend:
server-ip=127.0.0.1
server-port=25566
online-mode=false
enable-status=false
enable-query=false
enable-rcon=false
network-compression-threshold=-1
Two of those lines deserve an explanation. online-mode=false is correct and in fact necessary on a backend behind a proxy, because the Mojang login already happened on the proxy and cannot happen a second time. That same line is also the reason why a reachable backend is so dangerous. network-compression-threshold=-1 turns off compression between proxy and backend, because both sit on the same machine and compressing there only burns CPU time. If a backend sits on a different machine, leave the value at 256. The basic installation of such a server is described in Install a Minecraft server on Debian.
If the backends live on other machines, you need a real firewall. With nftables, written out as a complete file so that the rules survive a reboot:
table inet mcbackend {
chain input {
type filter hook input priority filter ; policy drop ;
iif lo accept
ct state established,related accept
ct state invalid drop
ip saddr 203.0.113.10 tcp dport 22 accept
ip saddr 198.51.100.7 tcp dport 25566-25575 accept
icmp type echo-request limit rate 5/second accept
counter drop
}
}
The file goes to /etc/nftables.conf with the line #!/usr/sbin/nft -f above it. Then run systemctl enable --now nftables and, without fail, nft list ruleset to verify. The address 198.51.100.7 stands for your proxy here, 203.0.113.10 for the place you administer from. If you prefer UFW, this achieves the same thing, and the order matters so that you do not lock yourself out:
ufw allow from 203.0.113.10 to any port 22 proto tcp comment 'SSH office'
ufw allow from 198.51.100.7 to any port 25566:25575 proto tcp comment 'proxy to backends'
ufw default deny incoming
ufw default allow outgoing
ufw --force enable
ufw status numbered
The full walkthrough including the escape route is in Set up the UFW firewall without locking yourself out. Afterwards check from a foreign machine whether the backends really are closed. Do not assume, look:
nmap -Pn -p 25565-25580 YOUR.BACKEND.IP.ADDRESS
nmap -Pn -p- --min-rate 1000 YOUR.PROXY.IP.ADDRESS
The expected output for the backend is filtered on every port. If it says open, you have the problem this chapter is about. For the proxy, exactly one port is open, namely the one it listens on.
Why Pterodactyl sets its own trap here
If you manage your servers through a panel, this is easy to miss. Pterodactyl gives every instance an allocation consisting of an IP address and a port, and the default is the public address of the node, not 127.0.0.1. A backend you create in the panel is therefore reachable from the internet from the very first second, even if the proxy sits right next to it. Create an allocation on 127.0.0.1 for the backends and assign it to the instance. Installing the panel is covered in Install the Pterodactyl panel.
The same applies to Docker containers. A mapping like -p 25566:25565 publishes the port on every address of the machine and bypasses the UFW chains while doing so, because Docker writes its rules into the DOCKER-USER chain ahead of the filter rules. The correct form is -p 127.0.0.1:25566:25565.
The forwarding secret: why a reachable backend gives away operator privileges
Now to the point that turns an availability problem into a security problem. For a backend to know who is actually connected, the proxy has to tell it the real IP address, the UUID and the player profile. There are two fundamentally different ways to do that.
BungeeCord ip_forward: old compatibility, no verification
The older method is called ip_forward in BungeeCord and legacy in Velocity. It works by repurposing the address field of the handshake packet: the proxy appends the real player IP, the UUID and the profile properties behind the actual address, separated by null bytes. The backend splits that field apart and believes the result. It is enabled on both sides:
ip_forward: true # config.yml of the proxy
settings: # spigot.yml of the backend
bungeecord: true
The decisive sentence: this method contains no cryptographic verification whatsoever. The backend has no way of telling whether the handshake packet really came from your proxy. That is why the PaperMC documentation calls BungeeCord-style forwarding fundamentally insecure in so many words.
Let us work out what that means in practice. A backend behind a proxy necessarily runs with online-mode=false, because the Mojang check happened on the proxy. If that backend is reachable from the internet, anyone can connect to it directly with an off-the-shelf tool and set the appended fields freely. They pick a name, a UUID, a source address. A reachable backend with legacy forwarding means that anyone can impersonate any player, including your operators. It takes no bug in your software and no stolen password, only the address and the port number.
Velocity modern forwarding: a shared key instead of trust
Velocity solves the problem at the root. Modern forwarding does not transmit the IP address, UUID and profile inside the handshake but as a separate step of the login sequence, and it signs that data with a shared secret. The backend verifies the signature and rejects anything that does not come from a proxy holding the same key.
Velocity creates the key itself on first start. In velocity.toml:
player-info-forwarding-mode = "modern"
forwarding-secret-file = "forwarding.secret"
On every Paper backend from 1.19.4 onwards, the counterpart lives in config/paper-global.yml:
proxies:
velocity:
enabled: true
online-mode: true
secret: 'CONTENT_OF_THE_FORWARDING_SECRET_FILE'
Three notes from practice. The value of online-mode under proxies.velocity has to match online-mode in velocity.toml, otherwise your players get different UUIDs and lose their inventories. The forwarding.secret file is UTF-8, must not be empty, and tolerates neither a trailing space nor a trailing newline. And on Paper versions up to 1.18.2 the section is not proxies.velocity in paper-global.yml but settings.velocity in paper.yml.
The limitation of modern forwarding is the minimum version: it requires Minecraft 1.13 or newer on the backend. If you still run 1.8 servers in your network, you cannot use it there.
BungeeGuard as an interim solution for old versions
BungeeGuard exists for exactly that case. The plugin appends a secret token to the profile properties of legacy forwarding, and on the backend a counterpart checks whether that token is on its list. Without a valid token the connection is refused. Velocity supports the method natively:
player-info-forwarding-mode = "bungeeguard"
forwarding-secret-file = "forwarding.secret"
That is a genuine improvement over bare ip_forward, but the order still holds: BungeeGuard is the answer for networks that sit on machines without their own firewall or that have to serve old protocol versions. On your own server the firewall rule remains the first measure, and BungeeGuard comes in as the second layer. The authors of the plugin say as much themselves.
The four forwarding modes compared
| Mode | Where it is set | What is transmitted | Verification | Backend from |
|---|---|---|---|---|
none |
Velocity: player-info-forwarding-mode = "none" |
nothing, every player appears with the proxy address and an offline UUID | not applicable | any |
legacy (BungeeCord ip_forward) |
BungeeCord: ip_forward: true, backend: settings.bungeecord: true |
IP, UUID and profile properties appended to the handshake address field | none, the backend believes everyone | 1.7.2 |
bungeeguard |
Velocity: player-info-forwarding-mode = "bungeeguard", backend: plugin with a token list |
same as legacy, plus a secret token in the profile properties | shared secret, checked by the plugin | 1.7.2 |
modern |
Velocity: player-info-forwarding-mode = "modern", backend: proxies.velocity.enabled: true |
IP, UUID and profile as a separate step of the login sequence | signature over the shared key, checked by the server | 1.13 |
The conclusion is unambiguous: modern forwarding wherever the versions allow it, BungeeGuard where they do not, and in both cases the firewall rule from the previous chapter on top. The none mode is not a security gain, it merely loses the real IP addresses, which makes every ban system and every abuse investigation worthless.
What the status response reveals about your network
The server list query is the part of the protocol that runs before any login. The client opens a TCP connection, sends a handshake packet with next state 1, requests the status and receives a JSON document. No credentials, no account, no trace in the login log. By default the answer contains:
{
"version": { "name": "1.21.4", "protocol": 769 },
"players": {
"max": 500,
"online": 37,
"sample": [ { "name": "PlayerName", "id": "…" } ]
},
"description": { "text": "Our network" },
"enforcesSecureChat": true,
"favicon": "data:image/png;base64,…"
}
Three fields are interesting to an attacker. players.online tells him when an attack is worth it, meaning evenings and weekends. version.protocol tells him which protocol bugs might apply to you. And players.sample hands him names and UUIDs of real players that he can use for targeted bot joins or for impersonation attempts.
Trimming the status response
On the backends you turn the status response off completely. A backend behind a proxy does not need it, because it will never appear in a server list. In server.properties:
enable-status=false
The server then stops answering status queries altogether and shows up in scans as a port that is open but silent. If you need the status response on a backend for operational reasons, at least trim the player list:
hide-online-players=true
On the proxy itself the status response obviously stays on, otherwise your network disappears from your players' server lists. You can, however, control how much it says. In Velocity:
show-max-players = 500
sample-players-in-ping = false
ping-passthrough = "disabled"
With sample-players-in-ping = false, which is the default, Velocity shows no name list when a player hovers over the player count. With ping-passthrough = "disabled" the proxy answers the query from its own configuration and does not contact a backend for it. That is a protective measure in itself: with "all" or "description", every incoming status query triggers a query to a backend, so a ping flood reaches servers that are not even exposed to the internet.
BungeeCord has a cache for the same purpose, and it is off by default:
remote_ping_cache: 10000
remote_ping_timeout: 5000
log_pings: false
The value -1 disables the cache and every query then passes through to the backend. With 10000 the proxy serves the same cached answer for ten seconds. log_pings: false is not a protective measure as such, but it stops a ping flood from filling your disk with log lines, and proxies die of that more often than of the packet load itself.
The four attack types that hit a Minecraft proxy
Attacks against a proxy differ from a pure bandwidth flood in that they use the protocol. They are therefore cheap, look like real players at first glance, and cannot be identified by data volume alone.
| Attack type | What the attacker sends | What it costs the proxy | What you see in the logs |
|---|---|---|---|
| Connection flood on 25565 | huge numbers of TCP connections, often without a single Minecraft packet afterwards | one socket, one connection tracking entry and network stack work per connection | thousands of entries in ss -tn state syn-recv, rising ListenOverflows |
| Handshake without login | a complete TCP setup, then a handshake with next state 2 and nothing after that | a half-open login sequence per connection that holds memory until the timeout | connections in ESTAB with no matching player, memory rising without player growth |
| Ping flood on the server list | handshake with next state 1 plus a status request, once per second from many sources | JSON generation per request, plus a backend query if ping-passthrough is on |
an exploding log file, on BungeeCord because of log_pings: true |
| Bot joins with a valid handshake | complete logins, with made-up names in offline mode and with bought accounts in online mode | a full player state on proxy and backend, occupied slots, chunk loading | names following a pattern, all from a few network ranges, joins once per second |
The first and second type cost the attacker almost nothing. Opening a TCP connection and leaving it open is one entry in a loop on his side and a socket with kernel buffers plus an object in the proxy's Java heap on yours. The ratio is the actual problem: the attacker pays in bytes, you pay in memory.
The fourth type is the only one that cannot be solved purely at the network level, because it is protocol compliant. If your network runs in online mode it is also expensive for the attacker, because every account costs money and is lost after a ban. If it runs in offline mode it is free, and then you need a verification step in front.
Countermeasures on the proxy, with concrete configuration blocks
1. Connection limits per source address with nftables
The most effective local measure against a connection flood is a limit per source address, applied in the kernel before the proxy even learns about it. With nftables that takes two dynamic sets: one for concurrently open connections, one for the rate of new ones.
table inet mcproxy {
set concurrent {
type ipv4_addr
size 131072
flags dynamic
}
set newrate {
type ipv4_addr
size 131072
flags dynamic,timeout
timeout 2m
}
chain input {
type filter hook input priority filter ; policy accept ;
tcp dport 25565 ct state new add @concurrent { ip saddr ct count over 12 } counter drop
tcp dport 25565 ct state new add @newrate { ip saddr limit rate over 20/minute burst 10 packets } counter drop
}
}
The first rule drops new connections as soon as the same source address has more than twelve open at once. The second drops them as soon as the same source address opens more than twenty new connections per minute, with a burst of ten for the normal startup sequence. Both numbers are starting points, not truths. A single player opens several connections when the client starts, because the server list queries every entry separately, and behind a large connection several players share one address. Measure a week of normal operation before you tighten anything.
If the attack is happening right now and you need something fast, the same thing in iptables:
iptables -I INPUT -p tcp --dport 25565 --syn -m connlimit --connlimit-above 12 --connlimit-mask 32 -j DROP
iptables -I INPUT -p tcp --dport 25565 --syn -m hashlimit --hashlimit-name mcnew --hashlimit-mode srcip --hashlimit-above 20/min --hashlimit-burst 10 -j DROP
iptables -L INPUT -n -v --line-numbers
The third line is the important one: if the hit counters do not rise, your rule is never reached. With UFW active, custom rules otherwise sit behind the UFW chains and do nothing. To make them permanent they belong in /etc/ufw/before.rules, or you save them with apt-get install -y iptables-persistent and netfilter-persistent save.
2. Half-open connections and the kernel accept queue
Against a pure SYN flood there is a mechanism Linux brings along and that, unlike with UDP, really works for TCP. SYN cookies are a technique where the kernel does not store the state of a half-open connection but encodes it into the sequence number of its reply, so that a flood of forged connection requests no longer consumes memory:
sysctl -w net.ipv4.tcp_syncookies=1
sysctl -w net.ipv4.tcp_max_syn_backlog=8192
sysctl -w net.core.somaxconn=8192
sysctl -w net.ipv4.tcp_synack_retries=2
sysctl -w net.ipv4.tcp_abort_on_overflow=0
To make this permanent, the lines go without -w into /etc/sysctl.d/99-mcproxy.conf, followed by sysctl --system. The somaxconn value is the upper bound for the queue of fully established connections that the application has not accepted yet. If it overflows, the kernel drops silently, and the counter for that is:
nstat -az | grep -E 'TcpExtListenOverflows|TcpExtListenDrops|TcpExtSyncookiesSent'
ss -ltn '( sport = :25565 )'
If TcpExtListenOverflows rises, the proxy is not accepting connections fast enough. The Recv-Q column of the second command shows the current fill level of that queue, the Send-Q column shows its limit. That is the hardest evidence of whether the bottleneck is in the kernel or in the proxy.
3. connection_throttle in BungeeCord and Waterfall
BungeeCord ships with a built-in connection throttle, and it is on by default. In config.yml:
connection_throttle: 4000
connection_throttle_limit: 3
timeout: 30000
server_connect_timeout: 5000
player_limit: -1
max_packets_per_second: 4096
max_packets_data_per_second: 33554432
The first two values belong together: once an IP address has connected connection_throttle_limit times within connection_throttle milliseconds, it has to wait out that period before it may connect again. The defaults therefore allow three connections in four seconds per address. For a network under fire that is generous, and 8000 milliseconds with a limit of 2 is a reasonable next step. Beyond 10000 milliseconds you will regularly lock out your own players who want to reconnect right after a crash.
max_packets_per_second and max_packets_data_per_second are the packet limits BungeeCord enforces per connection: 4096 packets per second and 32 MiB per second. They hit exactly the patterns a single client never produces, and they keep one connection from occupying the proxy on its own.
timeout is the read timeout for the player connection, server_connect_timeout the wait when connecting to a backend. Do not set the first one too low, or players with a fluctuating connection get dropped, and not too high, or half-open sessions hold their memory for a long time.
4. login-ratelimit and timeouts in Velocity
Velocity calls the same throttle something else and configures it in the [advanced] section of velocity.toml:
[advanced]
login-ratelimit = 3000
connection-timeout = 5000
read-timeout = 30000
compression-threshold = 256
command-rate-limit = 50
kick-after-rate-limited-commands = 0
tab-complete-rate-limit = 10
log-player-connections = true
enable-reuse-port = false
login-ratelimit is the minimum time in milliseconds that must pass between two connections from the same IP address, three seconds by default, and a value of 0 disables it. connection-timeout applies to opening the connection to the backend, read-timeout to reading from a connection. command-rate-limit allows one command every 50 milliseconds, that is twenty per second, which catches command floods from compromised accounts.
enable-reuse-port is the setting worth knowing under load. By default Velocity accepts connections in a single thread and distributes them afterwards. With true the proxy uses the SO_REUSEPORT kernel feature so several threads accept at the same time. On a multi-core system with very many incoming connections this is exactly the bottleneck that makes the accept queue from point 2 overflow. The feature requires Linux, and it wants testing before the emergency.
5. Closing off the login path
Three settings decide how expensive a bot join is for the attacker. In velocity.toml:
online-mode = true
force-key-authentication = true
prevent-client-proxy-connections = false
kick-existing-players = false
online-mode = true is the single most important line of this whole article for everything that is not pure bandwidth. It enforces the Mojang login on the proxy, so every bot costs a purchased account. In BungeeCord the same setting is called online_mode: true and is likewise on by default.
prevent-client-proxy-connections kicks players whose network operator, according to the Mojang authentication server, differs from the one the connection comes from. That hits anonymization services, but it also hits players on mobile networks or corporate connections. Velocity itself calls it a weak form of protection. Only enable it if you have a concrete problem, and expect complaints. The BungeeCord counterpart is called prevent_proxy_connections and is false by default.
kick-existing-players decides what happens when the same account connects a second time. With false the new connection is refused, with true the old one is dropped. Under fire false is the calmer choice, because otherwise an attacker with a stolen account keeps throwing out your real players.
6. A queue and a verification step in front
A queue helps against bot joins and against join waves after a restart. The principle: after login the player does not land on the lobby right away but in a minimal waiting area, and from there players are released at a fixed rate. The waiting area is not a full server but an empty world without world generation, without entities and without plugins, provided by the proxy itself.
You gain two things. First, a join wave no longer hits your real backends but an area where a player costs a few kilobytes instead of a loaded chunk region. Second, you have a place to sort out bots before they touch anything: a bot that stubbornly sends a join packet behaves differently from a real client that reacts to gravity, sends its client settings and answers transactions.
For Velocity this family exists ready to use: LimboAPI provides the virtual waiting areas, LimboQueue builds the queue on top of it and LimboFilter the verification step including a falling check and an optional picture puzzle. Standalone lightweight waiting areas such as NanoLimbo can be registered as a normal backend with both BungeeCord and Velocity. Which tool you pick is secondary, the principle is the gain.
One note on ordering: a queue does not solve a bandwidth problem. It acts on logins, not on packets. Once your uplink is saturated, nobody reaches the waiting area either.
7. Keeping an eye on kernel connection tracking
A bottleneck that hits early during a connection flood: the kernel creates an entry in the connection tracking table for every connection. If the table fills up, it drops packets from real players too, and the system log says nf_conntrack: table full, dropping packet.
sysctl net.netfilter.nf_conntrack_count net.netfilter.nf_conntrack_max
conntrack -S
dmesg -T | grep -i conntrack | tail -20
If the counter sits permanently near the limit, raise the table and shorten the timeout for half-open connections at the same time:
sysctl -w net.netfilter.nf_conntrack_max=1048576
sysctl -w net.netfilter.nf_conntrack_tcp_timeout_syn_recv=20
sysctl -w net.netfilter.nf_conntrack_tcp_timeout_established=3600
The third line deserves an explanation, because it would be dangerous with other services. The default for established connections is five days. With Minecraft you can go considerably lower, because the protocol sends keep alive packets regularly: the server sends them roughly every fifteen seconds, and if the answer does not arrive it drops the connection anyway. One hour is therefore generous. Also budget around 300 bytes of memory per entry, so a million entries cost you roughly 300 MB.
8. Collect measurements before it goes wrong
The step almost nobody takes in advance: establishing a baseline while everything is still normal. Without a normal value you cannot tell after an incident whether 900 new connections per minute were a lot or simply Friday evening. With apt-get install -y vnstat sysstat conntrack the measurement runs continuously.
sar -n DEV 1 10
sar -n TCP,ETCP 1 10
ss -s
ss -tn state syn-recv '( dport = :25565 or sport = :25565 )' | wc -l
nstat -az | grep -E 'TcpActiveOpens|TcpPassiveOpens|TcpAttemptFails|TcpExtListenOverflows'
ip -s link show eth0
Four values are especially telling on a proxy. TcpPassiveOpens counts incoming connections and is the number that explodes first during a connection flood. The count of connections in state SYN-RECV shows a SYN flood immediately. TcpExtListenOverflows proves that the proxy is no longer keeping up. And TcpAttemptFails rises when connections fail, which is a good early indicator of a saturated uplink. How to interpret these values is covered in Detect a DDoS attack on your server.
On top of that come the proxy's own metrics. In a Java application the used heap is the value that makes a flood of half-open logins visible first. If it fills up, the proxy aborts with an out of memory error and every player on the network is gone at once. How to measure and fix that is covered in Fix the Minecraft Java heap space error. If things stutter without conspicuous network values, a backend is usually the cause, and Fix Minecraft server lag is the matching article.
BungeeCord, Waterfall or Velocity: what matters for protection
Choosing a proxy stopped being a matter of taste when PaperMC announced the end of Waterfall on March 26, 2024. Where things stand:
| Property | BungeeCord | Waterfall | Velocity |
|---|---|---|---|
| Origin | SpigotMC, the oldest of the three projects | a BungeeCord branch at PaperMC | a ground-up project by PaperMC |
| Status in 2026 | still maintained | declared end of life by PaperMC on 2024-03-26, no promise of new Minecraft versions | actively developed, recommended by PaperMC |
| Default bind | 0.0.0.0:25577 |
0.0.0.0:25577 |
0.0.0.0:25565 |
| Forwarding | legacy ip_forward only, unverified |
same as BungeeCord, BungeeGuard via plugin | modern with a shared key, plus legacy and bungeeguard as fallbacks |
| Connection throttle | connection_throttle: 4000, connection_throttle_limit: 3 |
same as BungeeCord | login-ratelimit = 3000 |
| Packet and command limits | max_packets_per_second: 4096, max_packets_data_per_second: 33554432 |
same as BungeeCord | command-rate-limit = 50, tab-complete-rate-limit = 10, read-timeout = 30000 |
| Status query | remote_ping_cache, log_pings: true by default |
same as BungeeCord | ping-passthrough = "disabled" by default, show-ping-requests = false |
| Accepting on several cores | not available | not available | enable-reuse-port for SO_REUSEPORT on Linux |
Stated plainly: Velocity is newer, accepts connections more efficiently and is the only one of the three with a forwarding model that verifies anything cryptographically. Waterfall was the maintained BungeeCord branch until PaperMC ended the project, which makes it a poor choice for a new network. BungeeCord remains the right answer if you depend on plugins that exist nowhere else, or if you have to serve backends below 1.13. Anyone staying on BungeeCord has to take the firewall rule from the second chapter all the more seriously, because there it is the only thing standing between an attacker and any player profile he likes.
Where these measures stop: bandwidth and packet rate
Now the part no configuration file can solve. Every measure so far runs on your server, in other words at the end of the line. A firewall rule decides about a packet that has already traveled down the cable. You can drop it, but you cannot un-send it.
| Metric | Value |
|---|---|
| Typical uplink of a game server | 1 Gbps, which is 125 megabytes per second |
| Packets of 64 bytes that fit into 1 Gbps | around 1.49 million per second |
| What a normal server kernel handles of that | a few hundred thousand packets per second |
| Size of a SYN flood that fills 1 Gbps | around 1.9 million SYN packets per second at 66 bytes each |
| Typical attacks against Minecraft projects | 5 to 50 Gbps |
| Largest publicly documented attack on a Minecraft network | 2.5 Tbps in the third quarter of 2022, from a Mirai botnet, mixed UDP and TCP floods |
| Filtered in real time on KernelHost servers | over 473.4 Gbps at over 41.5 million packets per second against a voice server |
| Also filtered | a combined attack on 25565 TCP and 1194 UDP with over 16 attack patterns, over 4 million packets per second and over 8.6 Gbps |
Do the arithmetic. Your line is full as soon as somebody sends more than 125 megabytes per second. An attack of 5 to 50 Gbps is five to fifty times that. Whether your nftables rule behind it is any good no longer matters, because your players' packets never make it that far.
On a proxy there is a second limit that hits before bandwidth does. Every new connection costs CPU time in the kernel and in the proxy, and a login costs an additional request to the Mojang servers on top. An attack that does not even fill a tenth of your line can still make your proxy unreachable, because the accept queue overflows. Operators experience this as "the load was not high at all and yet nobody could get in". In game the same thing shows up as a connection timeout, while the players who are already connected keep playing for a while.
There is no local setting for this. Volumetric attacks and large connection floods have to end in the network in front of the server.
Reverse proxy networks such as TCPShield: a different approach
A common route among Minecraft networks is an upstream reverse proxy service, TCPShield being the best known, with NeoProtect and similar providers working the same way. The principle: you point a CNAME in DNS at the provider's network, players connect there, the service filters and forwards the clean traffic to your proxy. The real player IP arrives at your end through a plugin or the PROXY protocol, and your firewall only allows the provider's address ranges on 25565.
That is a clean approach and a good solution for many networks, especially when the server sits with a provider that does no filtering of its own. Three points are worth knowing, and none of them is a criticism, they are simply properties of the model.
First, the protection hangs on the firewall rule. Anyone who sets the CNAME but leaves the proxy open to the whole world will keep being attacked directly. The provider's address list has to be maintained, otherwise you lock out your own players after the network is expanded.
Second, the model protects the game traffic, not the server. Your real address stays attackable as soon as somebody knows it, and it still sits in every old DNS record. Other services on the same machine, a web panel or a database for instance, are outside that protection.
Third, the traffic is visible at one additional place. That is true of any upstream service and is not a special case.
The difference to filtering inside the hosting provider's own network is the location: there the protection sits on the path to your server, without a second party having to forward the traffic and without your address appearing a second time. Both work, they are different designs.
What KernelHost puts up against DDoS attacks on Minecraft proxies
The always-on protection included with every server
DDoS protection at KernelHost is built in two layers and permanently active, without you having to switch anything on, order anything or configure anything:
- Layer 1: 17 Tbps of mitigation capacity in the global scrubbing network. Volumetric attacks are cleaned up close to their source before they reach the data center.
- Layer 2: Arbor real-time filtering with 3.2 Tbps in Frankfurt am Main. Directly in front of the server, protocol-specific patterns are recognized and dropped, packet by packet. That includes connection floods on 25565 TCP as well as handshake patterns no Minecraft client produces.
Two properties are decisive. The protection runs permanently and does not have to react to an attack first, so there are no opening minutes in which the proxy is gone and your whole network with it. And no null-routing is used: your IP address stays on the network and only the malicious packets are dropped. Taking the IP address off the network achieves the same result for you as the attacker does. The location is Frankfurt am Main. Which games and protocols are covered is listed in Game server DDoS protection in real time, and how the filtering works is described in Specialized game DDoS protection with real-time filtering.
Advanced DDoS Protection, and why the protected IP belongs on the proxy
Some networks are attacked not occasionally but deliberately and for weeks on end, and that hits growing projects in particular. Advanced DDoS Protection exists for those cases, starting at €50.00 per month, PrePaid and with no minimum term. The difference is not more capacity but control:
- A dedicated protected IP from the Frankfurt core, which your server is moved onto inside our own network. Nothing has to be rebuilt on your side.
- Self-managed protection rules per port and protocol in the customer panel: you set separately what is allowed on 25565 TCP, what on 25577 TCP and what on 19132 UDP if Geyser is running alongside.
- Changes take effect in real time, so you can adjust during an ongoing attack instead of waiting for a maintenance window.
- A protection profile matching the game. There are ready-made profiles for Minecraft as well as for custom applications on arbitrary TCP or UDP ports, which covers a proxy on a port of your own choosing.
And now the sentence that matters for a proxy network: the protected IP belongs on the proxy, not on the backends. The proxy is the only address your players know, and therefore the only point where protection can work at all. After the second chapter of this article the backends should be unreachable from outside anyway, and a protected IP for an address nobody can reach protects nothing. If proxy and backends live on different machines, the proxy gets the protected IP and the backends get a firewall rule.
The two layers compared
| Property | Included always-on DDoS protection | Advanced DDoS Protection |
|---|---|---|
| Price | included with every server package at no surcharge | from €50.00 per month, PrePaid |
| Filtering capacity | 17 Tbps of global scrubbing plus Arbor real-time filtering with 3.2 Tbps in Frankfurt am Main | the same two-layer filtering |
| IP address | the IP address of your server | an additional dedicated protected IP, placed on the proxy |
| Rule set | automatic profiles, no configuration needed | your own rules per port and protocol in the customer panel |
| Changes | applied automatically | take effect in real time, even during an attack |
| Game profile | optimized profiles for common games, Minecraft included | a profile matching the game, including proxies on non-default ports |
| Null-routing | no | no |
| Term | tied to the server package | PrePaid, no minimum term, no notice period, no setup fee |
For most proxy networks the included always-on protection together with closed backends and a clean proxy configuration is enough. Advanced DDoS Protection is the answer to somebody taking it personally. If you currently run your network elsewhere, the most effective fix is moving it: the filtering works in the network in front of the server, and that network has to be ours.
Common mistakes and how to fix them
"Players with fake names are joining even though the proxy runs in online mode": your backends are reachable from the internet and the attackers connect to them directly. Verify with a port scan from outside, close the ports and switch to modern forwarding or BungeeGuard. As long as legacy forwarding runs without a firewall rule, anyone can claim any name and any UUID.
"After switching to Velocity all players lost their inventories": the value of proxies.velocity.online-mode on the backends does not match online-mode in velocity.toml. Both have to be identical, otherwise the server computes different UUIDs and creates a new profile for every player. Set the value back, the old profile files are still there under their original UUID.
"Velocity reports that the key is invalid": almost always there is a newline or a space at the end of the forwarding.secret file or inside the secret field of paper-global.yml. Check with od -c forwarding.secret | tail -2 whether the file really ends with the last character of the key.
"The proxy accepts no new connections although the load is low": that is the accept queue. Check nstat -az | grep ListenOverflows and ss -ltn '( sport = :25565 )'. If the counter rises and Recv-Q sits permanently at its limit, raise net.core.somaxconn and try enable-reuse-port = true on Velocity.
"My log file grew to 40 GB overnight": that is a ping flood combined with log_pings: true, the BungeeCord default. Set the value to false, and on Velocity check show-ping-requests and log-player-connections in the [advanced] section. Also set up rotation before a full disk stops the proxy. What to do immediately when the disk is full is covered in Disk full: finding and freeing up space on a Linux server.
"My nftables rule counts zero hits": three causes are common. The rule sits in a chain that is never traversed, it was not reloaded after the last reboot, or the attack is volumetric and the rule is working correctly on an uplink that is already saturated. Check with nft list ruleset and watch the counter values.
"My own players get dropped since I tightened the connection throttle": a single Minecraft client opens several connections in quick succession, because the server list queries every entry. On top of that come players sharing one address. Do not go below 2 for connection_throttle_limit and not above 5000 for login-ratelimit, and check your values against a week of normal operation.
"My previous provider blocked my IP address": that is null-routing. The provider is protecting its own network with it, and for you the result is identical to a successful attack, usually for hours afterwards. On a proxy network the damage is maximal, because the network has no second entrance. When in doubt, ask whether traffic is filtered or null-routed. That answer decides more about your availability than any hardware specification.
"I see nothing unusual in the capture": if the traffic is already being filtered in the network in front of you, nothing arrives on the server, exactly as intended. That is the normal picture with working filtering. Conversely, if the uplink is saturated, even the SSH session you wanted to measure with may not reach you. Use the VNC console in the customer panel then, which works independently of the guest system's network.
In short
- A Minecraft proxy network needs exactly one open port to the outside: the proxy port. Velocity listens on 25565 TCP by default, BungeeCord and Waterfall on 25577 TCP. Backends, query on 25565 UDP and RCON on 25575 TCP belong behind the firewall.
- Reachable backend servers are the most expensive mistake in the whole setup. They make every measure on the proxy useless, and with legacy forwarding they let anyone impersonate any player, including your operators.
- Velocity modern forwarding verifies the forwarded player data with a shared key and requires Minecraft 1.13 on the backend. BungeeCord
ip_forwardverifies nothing, BungeeGuard adds a token. The firewall rule replaces neither method and is replaced by neither. - The status response reveals the version, the player count and a sample of player names before anyone has logged in. Backends belong on
enable-status=false, the proxy onping-passthrough = "disabled"plus a cache for the queries. - The four attack types against a proxy are a connection flood on 25565, a handshake without login, a ping flood on the server list and bot joins with a valid handshake. The first three cost the attacker almost nothing and cost you memory.
- Locally, what helps is a limit per source address with
nftables, SYN cookies with a sufficiently large accept queue,connection_throttle: 4000withconnection_throttle_limit: 3on BungeeCord,login-ratelimit = 3000on Velocity, and a queue in front of the lobby. - From around 1 Gbps your line is full, and packets of 64 bytes fit into it around 1.49 million times per second. On a proxy the accept queue usually overflows before that. Beyond that point, only filtering in the network in front of the server decides the outcome.
- At KernelHost the two-layer always-on protection is included with every server package, active from provisioning and without null-routing. Advanced DDoS Protection adds a dedicated protected IP, and that protected IP belongs on the proxy.
If your network already runs at KernelHost, the filtering is active without you doing anything. If you still notice something unusual, open a support ticket so the filter rules for your IP address can be adjusted. During an ongoing attack you can also reach us through the WhatsApp emergency chat at +43 650 8209883.
Frequently asked questions
My Minecraft network is offline and the proxy does not respond. How do I tell whether it is a DDoS attack?
Which ports do I have to leave open for a Minecraft proxy network?
Why are reachable backend servers the most expensive mistake in a proxy network?
What is the difference between BungeeCord ip_forward and Velocity modern forwarding?
Do I still need BungeeGuard if I already have a firewall?
What does the status response of my Minecraft server reveal and how do I trim it?
Which attack types hit a Minecraft proxy?
How do I limit connections per source address on a Minecraft proxy?
What do connection_throttle in BungeeCord and login-ratelimit in Velocity actually do?
BungeeCord, Waterfall or Velocity: which is the better choice for DDoS protection?
Does an upstream reverse proxy service such as TCPShield help against DDoS attacks?
At what attack size can my proxy no longer handle it on its own?
Does my Minecraft network at KernelHost go offline during an attack?
Does DDoS protection at KernelHost cost extra, and does the protected IP go on the proxy or on the backends?
2026 KernelHost GmbH. All rights reserved. This guide is protected by copyright. Republishing it on other websites, in whole, in part or in edited form, is not permitted without our written consent. Quoting with a source credit and a link is expressly welcome.

