Anti-VPN and anti-proxy protection with country filtering, Discord alerts, a silent mode and a dry-run mode.
ProxyShield
Blocks VPNs, proxies, Tor exits and datacenter ranges at login — by asking several services at once and adding up what they say, instead of trusting one of them.
That difference is the whole plugin. Every anti-VPN queries an API and kicks on proxy: true. The
problem is what happens when that one service has never heard of the VPN in front of it: it answers
"clean", nothing else is asked, and the connection goes through. The list of VPNs any single free
dataset misses is long, and it is exactly the list of VPNs people actually pay for.
ProxyShield asks all of them, weighs the answers, and refuses the connection once enough of them agree. One blind provider can no longer wave a VPN through, and one noisy provider can no longer kick your players on its own.
| Build | Runs on |
|---|---|
ProxyShield-Paper-2.2.1.jar |
Paper, Purpur, Pufferfish and plain Spigot 1.21+ |
ProxyShield-Folia-2.2.1.jar |
Folia |
ProxyShield-Bungee-2.2.1.jar |
BungeeCord / Waterfall |
ProxyShield-Velocity-2.2.1.jar |
Velocity 3.3+ |
ProxyShield-Sponge-2.2.1.jar |
SpongeAPI 17 |
Install one jar, on the machine that accepts the connection. On a network that is the proxy — a Paper build behind BungeeCord only ever sees the proxy's own address. The Paper and Folia jars are built from the same sources and are interchangeable; both exist so people find a jar named after their platform.
Drop it in, restart, done. Seven providers are enabled out of the box and none of them needs an API key.
How a verdict is reached
Every enabled provider is queried at the same time. Each one carries a weight, the answers are
added up, and the connection is refused once the total reaches required-score. At the default of
1.5, one strong provider is enough on its own, or any two weaker ones, or one weak provider plus a
reverse-DNS hit.
api:
mode: consensus # or failover
required-score: 1.5
providers:
ip-api: { enabled: true, weight: 1.0 }
blackbox: { enabled: true, weight: 1.0 }
ipapi-is: { enabled: true, weight: 1.5 }
proxycheck: { enabled: false, weight: 2.0, key: "" }
Raise the score to 2.5 if you would rather miss a VPN than kick a real player. Lower it to 1.0
to block on any single hit, which is what most other anti-VPN plugins do by default.
mode: failover keeps the old behaviour — ask in order, first answer wins. It costs fewer requests
and detects less.
This does not multiply your quota the way it sounds. Answers are cached, simultaneous logins from one address share a single lookup, the fan-out stops the moment the score is reached, the cache survives restarts and reloads, and a provider that starts failing is dropped from the rotation until it recovers. In practice a busy server spends fewer requests than v2.1 did, not more.
26 services, 15 of them free
Enabling one is a single line. Each entry knows its own response layout, so nothing is guessed at.
No key needed: ip-api, blackbox, iprisk, ipquery, ipapi-is, iplocate, freeipapi,
funkemunky, negativity, fastasfuck, zombiebot, rayzs, marvinmc, ip-check, zowi
Key based: proxycheck, ipqualityscore, vpnapi, iphub, iphunter, ip2location,
vpnblocker, getipintel, antivpn-io, 2ip, abuseipdb
A service that needs a key stays inert until you add one, and says so once at start-up rather than
failing on every login. Anything not in the list can be described directly in config.yml with a
URL and dotted field paths — no code, no waiting for an update.
/proxyshield providers shows every enabled service with its weight, health and latency.
/proxyshield check <ip> shows what each one said about a specific address, which is what you want
open in front of you when somebody reports a false positive.
The signals that cost nothing
APIs are lists of addresses somebody already reported. An attack from a range rented this morning is invisible to all of them — but not to the server being attacked.
Network (ASN) rules. Address feeds always lag: a VPN rents a new range and the public lists catch
up days later. The network number behind it does not change nearly as often, so blocking the ASN
stops every address inside it at once. /proxyshield lookup <ip> prints the number to put in the
list.
Reverse DNS. Hosting and VPN operators name their machines after themselves, so a PTR record like
mad-01.torguard.io gives it away for free, with no quota and no rate limit. It adds weight rather
than blocking outright, because plenty of ordinary ISPs put words like host in their customer
names. Runs under a hard timeout so a slow resolver cannot stall a login.
Behaviour. Thirty accounts appearing out of one /24 inside a minute is a shape a normal player base does not produce. So is one address cycling through a dozen names, or an account whose country changes between two logins ten minutes apart. These add weight too, so a school or a Discord community joining together is not a mass kick.
Tor exit list. The most reliable feed there is — those addresses are published by the network itself.
Not kicking real players
The cost of an anti-VPN is not the VPN it misses. It is the regular who gets thrown out on a Friday evening and does not come back.
Trust. An account that has connected cleanly often enough, over long enough, stops being re-judged on every login. Mobile players behind carrier-grade NAT are the ones a hosting block hits hardest — their address can sit in a flagged range one evening and not the next — and this is what keeps them online. It never overrides your manual blacklist, and it is reset the moment an account is caught behind a proxy.
Connection type. Where a provider reports that an address is residential or mobile, the hosting block does not apply to it. Carriers land in datacenter ranges more often than you would think.
Dry-run. detection.dry-run: true reports everything and disconnects nobody. Run it for a day,
read blocks.log, then switch enforcement on knowing what it would have done.
Silent mode. No staff broadcast and no console line, while the log file and the Discord webhook keep recording. Watch the filter without announcing every detection to everyone online.
On a network
One server needs no setup — verdicts and player history live in two files next to the config.
A network of several proxies should share them, so each address is paid for once for the whole network instead of once per proxy, and every front end agrees about it:
storage:
type: redis # or mysql, mariadb, postgresql, sqlite, h2, file, memory
redis:
host: "127.0.0.1"
port: 6379
password: ""
Redis also pushes: whitelisting an address or running /proxyshield clearcache on one proxy takes
effect on the others immediately, rather than whenever the entry happens to expire.
No database driver and no Redis library is bundled, so nothing is relocated into a server that did
not ask for it. Most Bukkit servers already have a MySQL and a SQLite driver; if yours does not, drop
the driver jar into the ProxyShield folder and name it under storage.sql.driver-path.
If the store cannot be reached, the plugin says so once and falls back to file storage. A database outage never becomes a login outage.
Order of decision
Strictly top to bottom, first match wins. Everything that can allow a player is evaluated before anything that can block one, so you can always whitelist your way out of a false positive.
| # | Check | Result | Cost |
|---|---|---|---|
| 1 | Bypass permission | allow | none |
| 2 | Bedrock player (Geyser/Floodgate) | allow | none |
| 3 | Name on the player whitelist | allow | none |
| 4 | IP on the whitelist | allow | none |
| 5 | Loopback or private address | allow | none |
| 6 | IP on the blacklist | block | none |
| 7 | Trusted regular | allow | none |
| 8 | IP in a downloaded feed | block | ~0.6 µs |
| 9 | Network on the ASN block list | block | shared with 10 |
| 10 | Providers, reverse DNS and behaviour reach the score | block | one round per uncached address |
| 11 | Country not permitted | block | same round |
Steps 1 to 8 never touch the network.
Bedrock players are exempt by default. Wherever Geyser runs anywhere other than the player's own machine, the address seen here is Geyser's — and a Geyser instance on rented hardware sits in exactly the datacenter ranges the feeds list, so without that exemption a server would drop every Bedrock player the moment hosting detection was switched on.
Performance
Measured with the default feeds, 62,555 entries compiled into 35,161 merged ranges:
- Memory held — 1.3 MB
- Startup parse — 259 ms, on a background thread
- Feed check per login — 589 ns
- 1000 logins — 0.59 ms total, about 1/85th of one tick
The provider round is the only part with real latency. It runs off the main thread on every
platform, so it delays that one player's login and never the server tick, and it has a hard ceiling
(api.total-timeout-seconds) — whatever answered by then decides. Set use-api: false for a
feed-only setup with no network traffic per login at all.
Commands
All need proxyshield.admin. Aliases: /ps, /antivpn.
| Command | Does |
|---|---|
/proxyshield info |
Version, lookup mode, storage and list status |
/proxyshield providers |
Every enabled service with its weight, health and latency |
/proxyshield stats |
Detection counters, cache, webhook and efficiency |
/proxyshield check <player|ip> |
Runs the full pipeline and prints what each provider said |
/proxyshield lookup <player|ip> |
Fresh lookup ignoring the cache, with ASN and score |
/proxyshield whitelist <add|remove|list> [entry] |
Manage the never-block list |
/proxyshield blacklist <add|remove|list> [entry] |
Manage the always-block list |
/proxyshield refresh |
Re-download the feeds |
/proxyshield recheck |
Re-check everyone online, disconnect whoever fails |
/proxyshield clearcache |
Drop cached verdicts, locally and network-wide |
/proxyshield reload |
Re-read the config and the message file |
Permissions
| Node | Default | Grants |
|---|---|---|
proxyshield.bypass |
op | Skip every check |
proxyshield.notify |
op | Receive live block alerts |
proxyshield.admin |
op | Use /proxyshield |
On Paper, BungeeCord and Sponge no permission data exists yet during the login handshake, so
proxyshield.bypass only resolves for operators there. For reliable exemptions on those platforms
use the name or IP whitelist. On Velocity the permission works normally.
PlaceholderAPI
Available on the Bukkit builds when PlaceholderAPI is installed. Every placeholder reads the cache, so none of them can stall the main thread.
| Placeholder | Shows |
|---|---|
%proxyshield_blocked% %proxyshield_checked% |
Counters since start-up |
%proxyshield_blocked_proxy% %proxyshield_blocked_hosting% %proxyshield_blocked_country% |
Blocks by reason |
%proxyshield_country% %proxyshield_isp% %proxyshield_asn% |
The player's own address |
%proxyshield_is_proxy% %proxyshield_is_hosting% %proxyshield_score% |
The verdict on it |
%proxyshield_sources% |
Which services flagged it |
%proxyshield_trusted% |
Whether the player has earned an exemption |
%proxyshield_mode% %proxyshield_providers% %proxyshield_storage% |
How the plugin is set up |
%proxyshield_cache_size% %proxyshield_trusted_players% %proxyshield_uptime% |
Live state |
For other plugins
if (ProxyShieldApi.isAvailable() && ProxyShieldApi.isProxy(address)) {
// cache only, safe on the main thread
}
LookupResult result = ProxyShieldApi.lookup(address); // blocking, async only
boolean regular = ProxyShieldApi.isTrusted(uuid);
Read-only: it answers questions, it never changes a verdict or disconnects anybody.
Configuration
Everything behaves identically on all five platforms and lives in one config.yml. Player facing
text is separate, in messages_en.yml / messages_de.yml, so you can translate or reword anything
without touching the jar.
The settings people change first
detection:
dry-run: false # detect and report, never kick
block-proxy: true
block-hosting: true
block-tor: true
allow-residential-hosting: true # do not kick mobile carriers
asn:
enabled: true
blocked: [] # e.g. "AS9009" - M247, used by many commercial VPNs
allowed: [] # your own hosting, a school, a carrier
rdns:
enabled: true
weight: 1.0
api:
mode: consensus
required-score: 1.5 # 1.0 blocks on any single hit, 2.5 is cautious
total-timeout-seconds: 6 # ceiling for the whole lookup stage of one login
trust:
enabled: true
min-logins: 5
min-days: 3
heuristics:
enabled: true
max-joins-per-subnet-per-minute: 12
max-accounts-per-ip: 4
geo-jump-minutes: 10
country:
enabled: true
mode: whitelist # only these countries may connect
codes: ["DE", "AT", "CH"]
notifications:
silent: false # true = log and webhook only, no live alerts
Describing a service the plugin does not know
api:
providers:
my-provider:
enabled: true
url: "https://example.com/api/{ip}?key={key}"
key: ""
weight: 1.0
format: json # json or plain
proxy-fields: ["security.vpn", "security.proxy"]
hosting-fields: ["is_datacenter"]
country-fields: ["location.country_code"]
asn-fields: ["asn.asn"]
score-field: "risk_score"
score-threshold: 70
key-header: "" # set if the key goes in a header, e.g. X-Key
nested-by-ip: false # true if the answer is keyed by the address
Dotted paths work, so security.vpn reads {"security":{"vpn":true}}. For a service that answers
with a bare word:
my-simple-provider:
enabled: true
url: "https://example.com/check?ip={ip}"
format: plain
true-values: ["true", "y", "1"]
Metrics
Anonymous statistics through bStats: server count, platform, Java version and which ProxyShield
settings are in use. No addresses and no player data.
Public at bstats.org, and switched off with
metrics.enabled: false.
Support
Questions and bug reports: dc.gg/paperstream Source and issues: codeberg.org/LucasTHCR/ProxyShield
Pinned Versions
- R1.21–26.21.213.3–3.5
Pages
Members
1Owner