Welcome to the Hangar Open Beta. Please report any issue you encounter on GitHub!
Avatar for LucasTHCR

Anti-VPN and anti-proxy protection with country filtering, Discord alerts, a silent mode and a dry-run mode.

Report ProxyShield?

Discord Release Downloads Servers License

Built with Java 21 Available for Paper Available for Purpur Available for Spigot Available for Bukkit Available for Folia Available for Sponge Available for BungeeCord Available for Waterfall Available for Velocity

Watch us on YouTube Chat with us on Discord Available on Codeberg

# ProxyShield

Anti-VPN and anti-proxy protection for Minecraft servers. Country filtering, Discord webhook alerts, a silent mode and a dry-run mode. One shared detection core, five native builds that behave identically.

Build File Runs on
Paper ProxyShield-Paper-2.5.1.jar Paper, Purpur and Spigot 1.21+
Folia ProxyShield-Folia-2.5.1.jar Folia
BungeeCord ProxyShield-Bungee-2.5.1.jar BungeeCord / Waterfall
Velocity ProxyShield-Velocity-2.5.1.jar Velocity 3.3+
Sponge ProxyShield-Sponge-2.5.1.jar SpongeAPI 17
Universal ProxyShield-Universal-2.5.1.jar all five of the above, one file

The universal jar carries all four entry points and all four plugin descriptors at once. Every server reads the descriptor it knows and ignores the rest, so the same file drops into a Paper plugins/ folder, a BungeeCord one, a Velocity one or a Sponge mods/ folder. It costs about 400 KB over a single-platform jar, which buys one download for a network running several of them. The per-platform jars are still the plainer choice when you only run one.

The Paper and Folia jars are built from the same sources and are interchangeable. The scheduling already uses the regionised API, so either one runs on either server. They exist separately because server owners look for a jar named after their platform.

Spigot is supported by the same jar. Paper and Folia offer AsyncScheduler, a per-entity scheduler and Adventure components; Spigot has none of the three. The jar picks an implementation at start-up, and the Paper-only types live in a class that is never loaded on a server without them.

Install one jar, on the machine that accepts the connection. On a network that is the proxy (BungeeCord or Velocity). The Paper build behind a proxy would only ever see the proxy's own address.


Watch ProxyShield in action - a short demonstration of the checks, the verdict and what a blocked connection looks like.

Features

Detection

  • 26 IP intelligence services built in, enabled by name - 15 of them need no API key at all
  • Consensus scoring: several providers are asked at once and their weights added up, so one service that has never heard of a VPN cannot clear it, and one noisy service cannot kick anybody alone
  • Public VPN, datacenter and Tor exit feeds with correct CIDR matching, IPv4 and IPv6
  • Network (ASN) block and allow rules, which catch a VPN's new range before any feed lists it, plus downloadable ASN lists that refresh on their own
  • ISP filter with regular expressions, to block a hoster or always admit a mobile carrier
  • Conditions on any provider's answer (risk.score >= 85, connection.type = mobile) and custom request headers, so an unlisted service can be wired in exactly
  • Bogon addresses refused outright
  • Reverse DNS patterns as a signal - free, unlimited, and effective on ranges nobody has catalogued
  • Behavioural heuristics: join rate per subnet, accounts per address, an account changing country
  • Trust for regulars, so a tightened rule or a provider having a bad day does not kick your players
  • Country filtering, blacklist or whitelist mode
  • Manual blacklist, checked before everything else
  • Connection limit per address, by players online or within a time window
  • Configurable fail-open or fail-closed behaviour when a check cannot complete

Exemptions

  • IP whitelist accepting single addresses, CIDR blocks and ranges
  • Player name whitelist
  • Bypass permission
  • Loopback and private addresses are never checked, and never cost lookup quota
  • Bedrock players joining through Geyser/Floodgate are exempt by default

Operations

  • Dry-run mode: detect and report without disconnecting anybody
  • Passive mode: no checks at login, lookups on demand only
  • Restricted servers on BungeeCord and Velocity: keep a flagged player in the lobby instead of disconnecting them
  • Whitelist and blacklist entries with a reason and an expiry
  • /proxyshield alts lists every account seen on the same address
  • Debug output with every provider's raw answer
  • Console commands on a detection, so a flagged player can be marked or restricted instead of kicked
  • /proxyshield check shows what every single provider said, so a false positive can be traced
  • Lookup cache survives restarts and reloads; simultaneous logins from one address share one query
  • Shared storage for a network: MySQL, MariaDB, PostgreSQL, SQLite, H2 or Redis
  • PlaceholderAPI support and anonymous bStats metrics
  • Silent mode: no staff or console alerts, while the log file and webhook keep recording
  • Discord webhook with queueing, retry and rate-limit handling, and a layout you write yourself
  • Telegram and VK alerts
  • Block log at plugins/ProxyShield/blocks.log
  • Full /proxyshield command set with tab completion
  • English and German out of the box, any language by copying one file

Getting started

  1. Drop the jar for your platform into plugins/ and start the server once.
  2. Edit plugins/ProxyShield/config.yml.
  3. Run /proxyshield reload.

Recommended first run: leave detection.dry-run: true for a day and watch blocks.log. It records what would have been blocked without disconnecting anybody, which is the cheapest way to find false positives before they hit real players.

Providers and scoring

Enable a service by name. The fifteen listed as free need no key at all:

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: "" }

In consensus mode every enabled provider is asked at once and the weights of those flagging the address are added up. At required-score: 1.5 a single strong provider is enough, or any two weaker ones, or one weak provider plus a reverse DNS hit. Raise it 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.

This does not multiply your quota the way it looks. Answers are cached (IPv6 folded to its /64), simultaneous logins from one address share one 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.

A service the catalog does not know can be described directly in config.yml with a URL and dotted field paths - see the commented example at the bottom of the providers section.

Country filtering

country:
  enabled: true
  mode: whitelist        # only these countries may connect
  codes: ["DE", "AT", "CH"]
  block-unknown: false

Set mode: blacklist to invert it. Country data comes from the lookup provider, so country.enabled triggers a provider query even when detection.use-api is off.

Silent mode

notifications:
  silent: true

Suppresses the staff broadcast and the console line. The log file and the webhook still record every detection, so you keep the data without announcing to everyone online that the filter fired.

Commands on a detection

Not every server wants a flagged player gone. To mark them instead, run whatever you would have run by hand:

detection:
  dry-run: true

actions:
  on-flag:
    - "lp user {player} permission set myserver.vpnflagged true"
    - "webhook {player} flagged {reason} from {ip}"
  delay-seconds: 3
  run-when: always

With dry-run on, the detection still fires, the alert and the webhook still go out and these commands still run, but the player stays connected. That is the flag-instead-of-kick setup: put them in a restricted group, make them verify through Discord, hand the name to another plugin.

Commands run as the console, with or without a leading slash. Available placeholders are {player}, {uuid}, {ip}, {reason}, {reason_id}, {country}, {isp}, {asn}, {score}, {sources}, {action} and {platform}.

delay-seconds matters more than it looks. A connection is judged before the player is on the server, so at 0 the commands run while they are still connecting. That is fine for a permission grant or a webhook, but anything that needs them actually online, such as messaging them or prompting them to link an account, needs a few seconds here.

run-when accepts always, kicked or allowed when the commands should only fire for players who were disconnected, or only for those who were not.

Discord webhook

discord:
  enabled: true
  webhook-url: "https://discord.com/api/webhooks/..."
  show-ip: true

show-ip: false posts hidden instead of the address. Only https:// URLs are accepted.

The message layout is yours to write. By default it is a title and six inline boxes; clear the fields and write a description instead:

discord:
  embed:
    title: "`{player}`"
    description: |
      ```ansi
      IP: `{ip}`
      Country: {country}
      Action: {action}
      ```
    thumbnail: "https://your-server.example/alert.gif"
    fields: {}

Placeholders: {player} {uuid} {ip} {reason} {country} {isp} {asn} {score} {sources} {platform} {version} {action} {action_id}. Fields are named entries and appear in the order written.

Values go in exactly as written, so you place the backticks. Discord parses embed text as Markdown and an underscore is a legal Minecraft name character, so without a code span Some_Name shows up italic and __Name__ underlined with the underscores gone. Deleting the whole embed section keeps the layout from before 2.4.

Storage on a network

One server needs no setup - verdicts and trust live in two files. A network of several proxies should share them, so each address is looked up once for the whole network instead of once per proxy:

storage:
  type: redis            # or mysql, mariadb, postgresql, sqlite, h2, file, memory
  redis:
    host: "127.0.0.1"
    port: 6379
    password: ""

Redis also pushes invalidations: whitelisting an address or running /proxyshield clearcache on one proxy takes effect on the others immediately, rather than when the entry expires.

ProxyShield bundles no database driver and no Redis library, 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.

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

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.

API 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);

de.lucasthcr.bridgesave.proxyshield.api.ProxyShieldApi is read-only: it answers questions, it never changes a verdict or disconnects anybody.

Language

All player facing text lives in messages_en.yml and messages_de.yml. Point settings.language at the one you want. For another language, copy a file to messages_<code>.yml, translate it and set settings.language: <code>. Any key you delete falls back to the built-in English, so a message file from an older version keeps working after an update.


Commands

All of these need proxyshield.admin. Aliases: /ps, /antivpn.

Command Does
/proxyshield info Version, active mode, lookup mode and list status
/proxyshield providers Every enabled service with its weight, health and latency
/proxyshield stats Detection counters, cache and webhook state
/proxyshield check <player|ip> Runs the full pipeline and explains the verdict
/proxyshield lookup <player|ip> Fresh provider lookup, ignoring the cache
/proxyshield whitelist <add|remove|info|list|purge> [entry] [time] [reason] Manage the never-block list
/proxyshield blacklist <add|remove|info|list|purge> [entry] [time] [reason] Manage the always-block list
/proxyshield alts <player|ip> Every account seen on the same address
/proxyshield refresh Re-download the feeds
/proxyshield recheck Re-check everyone online, disconnect whoever fails
/proxyshield clearcache Drop cached lookup results
/proxyshield reload Re-read config.yml and the message file

Entries added in game are stored in whitelist.txt and blacklist.txt and survive restarts. Entries that came from config.yml have to be removed there.

/proxyshield check is the tool for a false-positive report: it walks the whole pipeline and prints what each stage decided, so you can see exactly which rule fired.

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 whitelist.players or whitelist.ips. On Velocity the player object is available at LoginEvent and the permission works normally.


How a connection is decided

Strictly top to bottom, first match wins. Everything that can allow a player is evaluated before anything that can block one, so an operator can always whitelist their way out of a false positive.

# Check Result Cost
1 proxyshield.bypass permission allow none
2 Name in whitelist.players allow none
3 IP in whitelist.ips allow none
4 Bedrock player via Floodgate allow none
5 Loopback or private address allow none
6 Bogon address block none
7 IP in blacklist.ips block none
8 IP in a downloaded feed block ~0.6 µs
9 Network (ASN) or ISP rule allow or block one HTTP request per uncached IP
10 Provider reports proxy block same request
11 Provider reports hosting block same request
12 Country not permitted block same request
13 Over the connection limit block none

Steps 1 to 8 never touch the network. Step 9 onwards costs one provider request per address that is not in the cache, which is why the cache TTL matters more than the rate limit on most servers.

Performance

Measured with the four default feeds (62,555 entries compiled into 35,161 merged ranges):

Memory held 1.3 MB
Startup parse 259 ms, on an async thread
Feed check per login 589 ns
1000 logins 0.59 ms total, about 1/85th of one tick

The provider lookup is the only part with real latency: typically 50 to 200 ms, capped by api.timeout-seconds. It runs off the main thread on every platform, so it delays that one player's login and never the server tick. Set detection.use-api: false for a pure feed setup with zero network traffic per login.


Building

Requires JDK 21 and Maven.

mvn clean package

The five plugin jars land in proxyshield-*/target/. Tests run as part of the build.

Module layout

proxyshield-common     detection core, no platform APIs, 124 tests
proxyshield-paper      Bukkit adapter    -> AsyncPlayerPreLoginEvent, Paper/Folia/Spigot
proxyshield-folia      same sources as proxyshield-paper, own descriptor and jar name
proxyshield-bungee     Bungee adapter    -> LoginEvent with intents
proxyshield-velocity   Velocity adapter  -> LoginEvent with EventTask.async
proxyshield-sponge     Sponge adapter    -> ServerSideConnectionEvent.Auth

Each platform module shades proxyshield-common, so every jar is self-contained. The Velocity and Sponge builds also bundle a relocated SnakeYAML, because neither exposes a YAML config API and both read the same config.yml as the others.

proxyshield-folia adds no Java sources of its own. It pulls in proxyshield-paper/src/main/java with build-helper-maven-plugin, so both jars are compiled from one copy of the code and a fix can never land in only one of them.

Tests

mvn -pl proxyshield-common test

Beyond the unit tests, two of them are worth knowing about: ShieldConfigTest parses the config.yml that actually ships in the jar and asserts every value the reader expects, and MessagesTest asserts that both bundled message files define exactly the keys the code looks up and that no key or value is swallowed by YAML's boolean coercion. Both of those caught real defects during development.


Changes from 1.1

See CHANGELOG.md. The short version: the IP feeds never matched anything in v1 because the CIDR prefix was thrown away, proxy detection was a substring test, the webhook payload was injectable, and BungeeCord and Velocity did blocking work on the wrong thread.

The original 1.1 sources are kept under legacy-1.1/ and the raw decompiler output under decompiled/ for reference.

Licence

GPL-3.0, see LICENSE.

Information

Category
Protection
Published
August 17, 2026
License
21Downloads
0Stars

Pinned Versions

  • R
    Paper1.21–26.2Waterfall1.21Velocity3.3–3.5

Members

1