September 2, 2026
Protect Login Pages with IP2Proxy Caddy Proxy Detection
Intro

By IP2Location
14 min read
Intro
Login pages are common targets for credential stuffing, password spraying, automated account takeover attempts, and other abusive activities. Attackers may use VPNs, Tor, residential proxies, data center servers, or other anonymizing networks to hide their original IP addresses and distribute login attempts across different networks.
However, blocking every visitor who connects through a proxy is not always the best approach. Legitimate users may also rely on VPNs, privacy services, or corporate networks.
Instead, proxy information can be treated as an additional risk signal when evaluating login attempts.
In this tutorial, we will use the IP2Proxy Caddy Middleware together with the commercial IP2Proxy PX12 BIN database to inspect an incoming connection before it reaches a PHP login page.
The Caddy middleware performs the IP2Proxy lookup and passes the resulting proxy intelligence to PHP through HTTP request headers. Based on this information, the PHP application can decide whether to allow normal authentication, require an additional verification step, or reject a high-risk login attempt.
The overall flow is:
Instead of using IP2Proxy as a simple proxy blocker, this tutorial demonstrates how proxy intelligence can become part of an adaptive authentication workflow.
The IP2Proxy Caddy Middleware supports local IP2Proxy BIN databases and remote queries through the IP2Location.io API. In this tutorial, we will use a local commercial PX12 BIN database, so lookups are performed directly on the server.
Why use the commercial IP2Proxy PX12 database?
The commercial PX12 database is suitable for this demonstration because it provides broad proxy coverage together with the additional fields needed for risk-based decisions.
PX12 can identify network types such as VPN, Tor, public proxies, web proxies, data center hosting, residential proxies, consumer privacy networks and enterprise private networks.
It also provides information including country, region, city, ISP, domain, usage type, ASN, last seen, threat, provider and Fraud Score.
The Fraud Score ranges from 0 to 99, giving the application an additional signal when evaluating login risk.
For this article, the application will evaluate several IP2Proxy fields rather than relying only on whether the IP is marked as a proxy.
Prerequisites
This tutorial uses Debian 13, Caddy, Go, xcaddy, PHP 8.4 with PHP-FPM, the IP2Proxy Caddy Middleware, and a commercial IP2Proxy PX12 BIN database.
Step 1: Update Debian 13
Update the package repository & upgrade installed packages:
sudo apt update
sudo apt upgrade -ysudo apt update
sudo apt upgrade -yStep 2: Install Caddy, Go, PHP and required packages
Install the required packages:
sudo apt install -y \
caddy \
golang-go \
php-fpm \
php-cli \
git \
unzip \
curl \
ca-certificatessudo apt install -y \
caddy \
golang-go \
php-fpm \
php-cli \
git \
unzip \
curl \
ca-certificatesVerify Go:
go versiongo versionVerify PHP:
php -vphp -vDebian 13 uses PHP 8.4 as its default PHP version.
Check PHP-FPM:
sudo systemctl status php8.4-fpmsudo systemctl status php8.4-fpmIf PHP-FPM is not running, enable and start it:
sudo systemctl enable --now php8.4-fpmsudo systemctl enable --now php8.4-fpmCheck the PHP-FPM Unix socket:
ls -l /run/php/php8.4-fpm.sockls -l /run/php/php8.4-fpm.sock
We will use this socket later.
Step 3: Install xcaddy
Caddy supports third-party modules, but custom modules need to be compiled into the Caddy binary.
Install xcaddy:
go install github.com/caddyserver/xcaddy/cmd/xcaddy@latestgo install github.com/caddyserver/xcaddy/cmd/xcaddy@latestAdd the Go binary directory to your PATH:
echo 'export PATH="$PATH:$HOME/go/bin"' >> ~/.profileecho 'export PATH="$PATH:$HOME/go/bin"' >> ~/.profileReload your shell profile:
source ~/.profilesource ~/.profileVerify the installation:
xcaddy versionxcaddy version
Caddy recommends xcaddy for building custom Caddy binaries.
Step 4: Build Caddy with the IP2Proxy middleware
Create a build directory:
mkdir -p ~/build-caddy-ip2proxymkdir -p ~/build-caddy-ip2proxyEnter the directory
cd ~/build-caddy-ip2proxycd ~/build-caddy-ip2proxyBuild Caddy with the IP2Proxy middleware
xcaddy build --with github.com/ip2location/ip2proxy-caddyxcaddy build --with github.com/ip2location/ip2proxy-caddyCheck the resulting binary:
./caddy version./caddy versionVerify that the IP2Proxy module is included:
./caddy list-modules | grep ip2proxy./caddy list-modules | grep ip2proxy
Step 5: Install the custom Caddy binary
The Debian Caddy package provides the systemd service and configuration directory, but its default binary does not contain the IP2Proxy module.
Use dpkg-divert so the package-managed binary is preserved:
sudo dpkg-divert \
--divert /usr/bin/caddy.default \
--rename /usr/bin/caddysudo dpkg-divert \
--divert /usr/bin/caddy.default \
--rename /usr/bin/caddyMove your custom Caddy binary:
sudo mv ./caddy /usr/bin/caddy.customsudo mv ./caddy /usr/bin/caddy.customSet executable permissions:
sudo chmod 755 /usr/bin/caddy.customsudo chmod 755 /usr/bin/caddy.customRegister the original Caddy binary:
sudo update-alternatives \
--install /usr/bin/caddy caddy /usr/bin/caddy.default 10sudo update-alternatives \
--install /usr/bin/caddy caddy /usr/bin/caddy.default 10Register the custom build:
sudo update-alternatives \
--install /usr/bin/caddy caddy /usr/bin/caddy.custom 50sudo update-alternatives \
--install /usr/bin/caddy caddy /usr/bin/caddy.custom 50Check which binary is active:
readlink -f /usr/bin/caddyreadlink -f /usr/bin/caddyIt should return:
/usr/bin/caddy.custom
Verify again:
caddy list-modules | grep ip2proxycaddy list-modules | grep ip2proxy
Step 6: Download the commercial PX12 BIN database
Download the commercial IP2Proxy PX12 BIN database from your IP2Location account.
Upload the downloaded ZIP file to your home folder like below:
~/ip2proxy-px12.zip
Create an extraction directory:
mkdir -p ~/ip2proxy-px12mkdir -p ~/ip2proxy-px12Extract the database:
unzip ~/ip2proxy-px12.zip -d ~/ip2proxy-px12unzip ~/ip2proxy-px12.zip -d ~/ip2proxy-px12Check the extracted files:
find ~/ip2proxy-px12 -type ffind ~/ip2proxy-px12 -type fYou should find a commercial PX12 BIN database similar to:
IP2PROXY-IP-PROXYTYPE-COUNTRY-REGION-CITY-ISP-DOMAIN-USAGETYPE-ASN-LASTSEEN-THREAT-RESIDENTIAL-PROVIDER-FRAUDSCORE.BIN
Step 7: Install the PX12 database
Create a directory for the database:
sudo mkdir -p /var/lib/ip2proxysudo mkdir -p /var/lib/ip2proxyMove the BIN database and give it a shorter filename:
sudo mv \
~/ip2proxy-px12/IP2PROXY-IP-PROXYTYPE-COUNTRY-REGION-CITY-ISP-DOMAIN-USAGETYPE-ASN-LASTSEEN-THREAT-RESIDENTIAL-PROVIDER-FRAUDSCORE.BIN \
/var/lib/ip2proxy/IP2PROXY-PX12.BINsudo mv \
~/ip2proxy-px12/IP2PROXY-IP-PROXYTYPE-COUNTRY-REGION-CITY-ISP-DOMAIN-USAGETYPE-ASN-LASTSEEN-THREAT-RESIDENTIAL-PROVIDER-FRAUDSCORE.BIN \
/var/lib/ip2proxy/IP2PROXY-PX12.BINSet ownership:
sudo chown -R root:caddy /var/lib/ip2proxysudo chown -R root:caddy /var/lib/ip2proxySet database permissions:
sudo chmod 640 /var/lib/ip2proxy/IP2PROXY-PX12.BINsudo chmod 640 /var/lib/ip2proxy/IP2PROXY-PX12.BINProtect the directory:
sudo chmod 750 /var/lib/ip2proxysudo chmod 750 /var/lib/ip2proxyVerify:
sudo ls -lh /var/lib/ip2proxy/sudo ls -lh /var/lib/ip2proxy/Check that Caddy can read the database:
sudo -u caddy test -r /var/lib/ip2proxy/IP2PROXY-PX12.BIN && echo "Caddy can read PX12"sudo -u caddy test -r /var/lib/ip2proxy/IP2PROXY-PX12.BIN && echo "Caddy can read PX12"Expected result:
Caddy can read PX12
Step 8: Create the PHP login application
Create the web directory:
sudo mkdir -p /var/www/ip2proxy-loginsudo mkdir -p /var/www/ip2proxy-loginCreate the PHP application:
sudo nano /var/www/ip2proxy-login/index.phpsudo nano /var/www/ip2proxy-login/index.phpPaste the following code:
<?php
session_start();
/*
* IP2Proxy Adaptive Login Demo
*
* Fixed credentials and verification codes are used only
* to make this demonstration self-contained.
*
* Do not use hard-coded authentication credentials in
* a production application.
*/
$demoUsername = 'demo';
$demoPassword = 'DemoPass123!';
$demoVerificationCode = '246810';
function getIP2ProxyHeader(string $name, string $default = '-'): string
{
$key = 'HTTP_X_IP2PROXY_' .
strtoupper(str_replace('-', '_', $name));
if (!isset($_SERVER[$key])) {
return $default;
}
return trim((string) $_SERVER[$key]);
}
function escape(string $value): string
{
return htmlspecialchars(
$value,
ENT_QUOTES | ENT_SUBSTITUTE,
'UTF-8'
);
}
/*
* Read the IP2Proxy information inserted by Caddy.
*/
$signalAvailable =
isset($_SERVER['HTTP_X_IP2PROXY_IS_PROXY']);
$isProxyRaw = getIP2ProxyHeader('IS_PROXY', 'Unknown');
$proxyType =
strtoupper(getIP2ProxyHeader('PROXY_TYPE'));
$countryCode =
getIP2ProxyHeader('COUNTRY_CODE');
$countryName =
getIP2ProxyHeader('COUNTRY_NAME');
$region =
getIP2ProxyHeader('REGION');
$city =
getIP2ProxyHeader('CITY');
$isp =
getIP2ProxyHeader('ISP');
$domain =
getIP2ProxyHeader('DOMAIN');
$usageType =
getIP2ProxyHeader('USAGE_TYPE');
$asn =
getIP2ProxyHeader('ASN');
$asName =
getIP2ProxyHeader('AS');
$lastSeen =
getIP2ProxyHeader('LAST_SEEN');
$threat =
getIP2ProxyHeader('THREAT');
$provider =
getIP2ProxyHeader('PROVIDER');
$fraudScoreRaw =
getIP2ProxyHeader('FRAUD_SCORE', '0');
$isProxy =
strcasecmp($isProxyRaw, 'True') === 0;
$fraudScore =
is_numeric($fraudScoreRaw)
? (int) $fraudScoreRaw
: 0;
/*
* Determine whether a threat classification is present.
*/
$normalizedThreat =
strtoupper(trim($threat));
$hasThreat =
$normalizedThreat !== '' &&
$normalizedThreat !== '-';
/*
* Some network types can still be interesting for login
* security even when Is-Proxy is False.
*
* DCH means Data Center / Web Hosting / Transit.
*/
$elevatedNetworkTypes = [
'DCH'
];
$isElevatedNetworkType =
in_array(
$proxyType,
$elevatedNetworkTypes,
true
);
/*
* Demo risk policy:
*
* HIGH
* Fraud Score >= 80
* OR a threat classification is present.
*
* ELEVATED
* Proxy detected
* OR Fraud Score >= 50
* OR Proxy Type = DCH
* OR IP2Proxy information is unavailable.
*
* LOW
* None of the above conditions apply.
*/
$shouldBlock =
$signalAvailable &&
(
$fraudScore >= 80 ||
$hasThreat
);
$requiresVerification =
!$shouldBlock &&
(
!$signalAvailable ||
$isProxy ||
$fraudScore >= 50 ||
$isElevatedNetworkType
);
if ($shouldBlock) {
$riskLevel = 'HIGH';
} elseif ($requiresVerification) {
$riskLevel = 'ELEVATED';
} else {
$riskLevel = 'LOW';
}
$state = 'login';
$message = '';
if (
isset($_SESSION['pending_login']) &&
$_SESSION['pending_login'] === true
) {
$state = 'verify';
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action =
$_POST['action'] ?? '';
/*
* Process username/password login.
*/
if ($action === 'login') {
$username =
(string) ($_POST['username'] ?? '');
$password =
(string) ($_POST['password'] ?? '');
if (
!hash_equals($demoUsername, $username) ||
!hash_equals($demoPassword, $password)
) {
$message =
'Invalid username or password.';
$state =
'login';
} elseif ($shouldBlock) {
unset($_SESSION['pending_login']);
http_response_code(403);
$state =
'blocked';
} elseif ($requiresVerification) {
$_SESSION['pending_login'] =
true;
$state =
'verify';
} else {
unset($_SESSION['pending_login']);
$state =
'success';
}
/*
* Process additional verification.
*/
} elseif ($action === 'verify') {
if (
!isset($_SESSION['pending_login']) ||
$_SESSION['pending_login'] !== true
) {
$message =
'Please log in first.';
$state =
'login';
} elseif ($shouldBlock) {
unset($_SESSION['pending_login']);
http_response_code(403);
$state =
'blocked';
} else {
$verificationCode =
(string) (
$_POST['verification_code'] ?? ''
);
if (
hash_equals(
$demoVerificationCode,
$verificationCode
)
) {
unset($_SESSION['pending_login']);
$state =
'success';
} else {
$message =
'Invalid verification code.';
$state =
'verify';
}
}
}
}
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta
name="viewport"
content="width=device-width, initial-scale=1"
>
<title>IP2Proxy Adaptive Login Demo</title>
<style>
body {
margin: 0;
padding: 40px 20px;
font-family: Arial, sans-serif;
background: #f5f7fa;
color: #222;
}
.container {
max-width: 780px;
margin: auto;
}
.card {
background: #fff;
padding: 28px;
margin-bottom: 20px;
border-radius: 10px;
box-shadow: 0 2px 12px rgba(0,0,0,.08);
}
h1,
h2 {
margin-top: 0;
}
input {
width: 100%;
box-sizing: border-box;
padding: 12px;
margin: 8px 0 16px;
border: 1px solid #ccc;
border-radius: 5px;
}
button {
padding: 12px 22px;
border: 0;
border-radius: 5px;
cursor: pointer;
background: #222;
color: #fff;
}
.error,
.blocked {
color: #b42318;
}
.success {
color: #067647;
}
.low {
color: #067647;
font-weight: bold;
}
.elevated {
color: #b54708;
font-weight: bold;
}
.high {
color: #b42318;
font-weight: bold;
}
table {
width: 100%;
border-collapse: collapse;
}
td {
padding: 9px 8px;
border-bottom: 1px solid #ddd;
vertical-align: top;
word-break: break-word;
}
td:first-child {
width: 38%;
font-weight: bold;
}
.note {
color: #666;
font-size: 14px;
line-height: 1.5;
}
</style>
</head>
<body>
<div class="container">
<div class="card">
<h1>Secure Login</h1>
<?php if ($message !== ''): ?>
<p class="error">
<?= escape($message) ?>
</p>
<?php endif; ?>
<?php if ($state === 'login'): ?>
<p>
Enter the demo credentials to continue.
</p>
<form method="post">
<input
type="hidden"
name="action"
value="login"
>
<label for="username">
Username
</label>
<input
id="username"
type="text"
name="username"
autocomplete="username"
required
>
<label for="password">
Password
</label>
<input
id="password"
type="password"
name="password"
autocomplete="current-password"
required
>
<button type="submit">
Sign in
</button>
</form>
<p class="note">
Demo username:
<strong>demo</strong>
<br>
Demo password:
<strong>DemoPass123!</strong>
</p>
<?php elseif ($state === 'verify'): ?>
<h2>
Additional verification required
</h2>
<p>
Your network connection has triggered additional
verification before the login can continue.
</p>
<form method="post">
<input
type="hidden"
name="action"
value="verify"
>
<label for="verification_code">
Verification code
</label>
<input
id="verification_code"
type="text"
name="verification_code"
inputmode="numeric"
autocomplete="one-time-code"
required
>
<button type="submit">
Verify
</button>
</form>
<p class="note">
Demo verification code:
<strong>246810</strong>
</p>
<?php elseif ($state === 'blocked'): ?>
<h2 class="blocked">
Login blocked
</h2>
<p>
This login attempt was rejected because the
network connection exceeded the configured
risk threshold.
</p>
<?php elseif ($state === 'success'): ?>
<h2 class="success">
Login successful
</h2>
<p>
Authentication has completed successfully.
</p>
<?php endif; ?>
</div>
<div class="card">
<h2>IP2Proxy risk signals</h2>
<p>
Risk level:
<span class="<?= strtolower($riskLevel) ?>">
<?= escape($riskLevel) ?>
</span>
</p>
<table>
<tr>
<td>IP2Proxy available</td>
<td><?= $signalAvailable ? 'Yes' : 'No' ?></td>
</tr>
<tr>
<td>Proxy detected</td>
<td><?= escape($isProxyRaw) ?></td>
</tr>
<tr>
<td>Proxy type</td>
<td><?= escape($proxyType) ?></td>
</tr>
<tr>
<td>Country</td>
<td>
<?= escape($countryCode) ?>
-
<?= escape($countryName) ?>
</td>
</tr>
<tr>
<td>Region</td>
<td><?= escape($region) ?></td>
</tr>
<tr>
<td>City</td>
<td><?= escape($city) ?></td>
</tr>
<tr>
<td>ISP</td>
<td><?= escape($isp) ?></td>
</tr>
<tr>
<td>Domain</td>
<td><?= escape($domain) ?></td>
</tr>
<tr>
<td>Usage type</td>
<td><?= escape($usageType) ?></td>
</tr>
<tr>
<td>ASN</td>
<td><?= escape($asn) ?></td>
</tr>
<tr>
<td>AS</td>
<td><?= escape($asName) ?></td>
</tr>
<tr>
<td>Last seen</td>
<td><?= escape($lastSeen) ?></td>
</tr>
<tr>
<td>Threat</td>
<td><?= escape($threat) ?></td>
</tr>
<tr>
<td>Provider</td>
<td><?= escape($provider) ?></td>
</tr>
<tr>
<td>Fraud score</td>
<td><?= escape($fraudScoreRaw) ?></td>
</tr>
</table>
<p class="note">
The IP2Proxy information is displayed only for this
demonstration. A production login page would normally
process these signals on the server without exposing
them to the visitor.
</p>
</div>
</div>
</body>
</html><?php
session_start();
/*
* IP2Proxy Adaptive Login Demo
*
* Fixed credentials and verification codes are used only
* to make this demonstration self-contained.
*
* Do not use hard-coded authentication credentials in
* a production application.
*/
$demoUsername = 'demo';
$demoPassword = 'DemoPass123!';
$demoVerificationCode = '246810';
function getIP2ProxyHeader(string $name, string $default = '-'): string
{
$key = 'HTTP_X_IP2PROXY_' .
strtoupper(str_replace('-', '_', $name));
if (!isset($_SERVER[$key])) {
return $default;
}
return trim((string) $_SERVER[$key]);
}
function escape(string $value): string
{
return htmlspecialchars(
$value,
ENT_QUOTES | ENT_SUBSTITUTE,
'UTF-8'
);
}
/*
* Read the IP2Proxy information inserted by Caddy.
*/
$signalAvailable =
isset($_SERVER['HTTP_X_IP2PROXY_IS_PROXY']);
$isProxyRaw = getIP2ProxyHeader('IS_PROXY', 'Unknown');
$proxyType =
strtoupper(getIP2ProxyHeader('PROXY_TYPE'));
$countryCode =
getIP2ProxyHeader('COUNTRY_CODE');
$countryName =
getIP2ProxyHeader('COUNTRY_NAME');
$region =
getIP2ProxyHeader('REGION');
$city =
getIP2ProxyHeader('CITY');
$isp =
getIP2ProxyHeader('ISP');
$domain =
getIP2ProxyHeader('DOMAIN');
$usageType =
getIP2ProxyHeader('USAGE_TYPE');
$asn =
getIP2ProxyHeader('ASN');
$asName =
getIP2ProxyHeader('AS');
$lastSeen =
getIP2ProxyHeader('LAST_SEEN');
$threat =
getIP2ProxyHeader('THREAT');
$provider =
getIP2ProxyHeader('PROVIDER');
$fraudScoreRaw =
getIP2ProxyHeader('FRAUD_SCORE', '0');
$isProxy =
strcasecmp($isProxyRaw, 'True') === 0;
$fraudScore =
is_numeric($fraudScoreRaw)
? (int) $fraudScoreRaw
: 0;
/*
* Determine whether a threat classification is present.
*/
$normalizedThreat =
strtoupper(trim($threat));
$hasThreat =
$normalizedThreat !== '' &&
$normalizedThreat !== '-';
/*
* Some network types can still be interesting for login
* security even when Is-Proxy is False.
*
* DCH means Data Center / Web Hosting / Transit.
*/
$elevatedNetworkTypes = [
'DCH'
];
$isElevatedNetworkType =
in_array(
$proxyType,
$elevatedNetworkTypes,
true
);
/*
* Demo risk policy:
*
* HIGH
* Fraud Score >= 80
* OR a threat classification is present.
*
* ELEVATED
* Proxy detected
* OR Fraud Score >= 50
* OR Proxy Type = DCH
* OR IP2Proxy information is unavailable.
*
* LOW
* None of the above conditions apply.
*/
$shouldBlock =
$signalAvailable &&
(
$fraudScore >= 80 ||
$hasThreat
);
$requiresVerification =
!$shouldBlock &&
(
!$signalAvailable ||
$isProxy ||
$fraudScore >= 50 ||
$isElevatedNetworkType
);
if ($shouldBlock) {
$riskLevel = 'HIGH';
} elseif ($requiresVerification) {
$riskLevel = 'ELEVATED';
} else {
$riskLevel = 'LOW';
}
$state = 'login';
$message = '';
if (
isset($_SESSION['pending_login']) &&
$_SESSION['pending_login'] === true
) {
$state = 'verify';
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action =
$_POST['action'] ?? '';
/*
* Process username/password login.
*/
if ($action === 'login') {
$username =
(string) ($_POST['username'] ?? '');
$password =
(string) ($_POST['password'] ?? '');
if (
!hash_equals($demoUsername, $username) ||
!hash_equals($demoPassword, $password)
) {
$message =
'Invalid username or password.';
$state =
'login';
} elseif ($shouldBlock) {
unset($_SESSION['pending_login']);
http_response_code(403);
$state =
'blocked';
} elseif ($requiresVerification) {
$_SESSION['pending_login'] =
true;
$state =
'verify';
} else {
unset($_SESSION['pending_login']);
$state =
'success';
}
/*
* Process additional verification.
*/
} elseif ($action === 'verify') {
if (
!isset($_SESSION['pending_login']) ||
$_SESSION['pending_login'] !== true
) {
$message =
'Please log in first.';
$state =
'login';
} elseif ($shouldBlock) {
unset($_SESSION['pending_login']);
http_response_code(403);
$state =
'blocked';
} else {
$verificationCode =
(string) (
$_POST['verification_code'] ?? ''
);
if (
hash_equals(
$demoVerificationCode,
$verificationCode
)
) {
unset($_SESSION['pending_login']);
$state =
'success';
} else {
$message =
'Invalid verification code.';
$state =
'verify';
}
}
}
}
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta
name="viewport"
content="width=device-width, initial-scale=1"
>
<title>IP2Proxy Adaptive Login Demo</title>
<style>
body {
margin: 0;
padding: 40px 20px;
font-family: Arial, sans-serif;
background: #f5f7fa;
color: #222;
}
.container {
max-width: 780px;
margin: auto;
}
.card {
background: #fff;
padding: 28px;
margin-bottom: 20px;
border-radius: 10px;
box-shadow: 0 2px 12px rgba(0,0,0,.08);
}
h1,
h2 {
margin-top: 0;
}
input {
width: 100%;
box-sizing: border-box;
padding: 12px;
margin: 8px 0 16px;
border: 1px solid #ccc;
border-radius: 5px;
}
button {
padding: 12px 22px;
border: 0;
border-radius: 5px;
cursor: pointer;
background: #222;
color: #fff;
}
.error,
.blocked {
color: #b42318;
}
.success {
color: #067647;
}
.low {
color: #067647;
font-weight: bold;
}
.elevated {
color: #b54708;
font-weight: bold;
}
.high {
color: #b42318;
font-weight: bold;
}
table {
width: 100%;
border-collapse: collapse;
}
td {
padding: 9px 8px;
border-bottom: 1px solid #ddd;
vertical-align: top;
word-break: break-word;
}
td:first-child {
width: 38%;
font-weight: bold;
}
.note {
color: #666;
font-size: 14px;
line-height: 1.5;
}
</style>
</head>
<body>
<div class="container">
<div class="card">
<h1>Secure Login</h1>
<?php if ($message !== ''): ?>
<p class="error">
<?= escape($message) ?>
</p>
<?php endif; ?>
<?php if ($state === 'login'): ?>
<p>
Enter the demo credentials to continue.
</p>
<form method="post">
<input
type="hidden"
name="action"
value="login"
>
<label for="username">
Username
</label>
<input
id="username"
type="text"
name="username"
autocomplete="username"
required
>
<label for="password">
Password
</label>
<input
id="password"
type="password"
name="password"
autocomplete="current-password"
required
>
<button type="submit">
Sign in
</button>
</form>
<p class="note">
Demo username:
<strong>demo</strong>
<br>
Demo password:
<strong>DemoPass123!</strong>
</p>
<?php elseif ($state === 'verify'): ?>
<h2>
Additional verification required
</h2>
<p>
Your network connection has triggered additional
verification before the login can continue.
</p>
<form method="post">
<input
type="hidden"
name="action"
value="verify"
>
<label for="verification_code">
Verification code
</label>
<input
id="verification_code"
type="text"
name="verification_code"
inputmode="numeric"
autocomplete="one-time-code"
required
>
<button type="submit">
Verify
</button>
</form>
<p class="note">
Demo verification code:
<strong>246810</strong>
</p>
<?php elseif ($state === 'blocked'): ?>
<h2 class="blocked">
Login blocked
</h2>
<p>
This login attempt was rejected because the
network connection exceeded the configured
risk threshold.
</p>
<?php elseif ($state === 'success'): ?>
<h2 class="success">
Login successful
</h2>
<p>
Authentication has completed successfully.
</p>
<?php endif; ?>
</div>
<div class="card">
<h2>IP2Proxy risk signals</h2>
<p>
Risk level:
<span class="<?= strtolower($riskLevel) ?>">
<?= escape($riskLevel) ?>
</span>
</p>
<table>
<tr>
<td>IP2Proxy available</td>
<td><?= $signalAvailable ? 'Yes' : 'No' ?></td>
</tr>
<tr>
<td>Proxy detected</td>
<td><?= escape($isProxyRaw) ?></td>
</tr>
<tr>
<td>Proxy type</td>
<td><?= escape($proxyType) ?></td>
</tr>
<tr>
<td>Country</td>
<td>
<?= escape($countryCode) ?>
-
<?= escape($countryName) ?>
</td>
</tr>
<tr>
<td>Region</td>
<td><?= escape($region) ?></td>
</tr>
<tr>
<td>City</td>
<td><?= escape($city) ?></td>
</tr>
<tr>
<td>ISP</td>
<td><?= escape($isp) ?></td>
</tr>
<tr>
<td>Domain</td>
<td><?= escape($domain) ?></td>
</tr>
<tr>
<td>Usage type</td>
<td><?= escape($usageType) ?></td>
</tr>
<tr>
<td>ASN</td>
<td><?= escape($asn) ?></td>
</tr>
<tr>
<td>AS</td>
<td><?= escape($asName) ?></td>
</tr>
<tr>
<td>Last seen</td>
<td><?= escape($lastSeen) ?></td>
</tr>
<tr>
<td>Threat</td>
<td><?= escape($threat) ?></td>
</tr>
<tr>
<td>Provider</td>
<td><?= escape($provider) ?></td>
</tr>
<tr>
<td>Fraud score</td>
<td><?= escape($fraudScoreRaw) ?></td>
</tr>
</table>
<p class="note">
The IP2Proxy information is displayed only for this
demonstration. A production login page would normally
process these signals on the server without exposing
them to the visitor.
</p>
</div>
</div>
</body>
</html>Save the file.
Set ownership:
sudo chown -R www-data:www-data /var/www/ip2proxy-loginsudo chown -R www-data:www-data /var/www/ip2proxy-loginSet directory permissions
sudo find /var/www/ip2proxy-login \
-type d \
-exec chmod 755 {} \;sudo find /var/www/ip2proxy-login \
-type d \
-exec chmod 755 {} \;Set file permissions:
sudo find /var/www/ip2proxy-login \
-type f \
-exec chmod 644 {} \;sudo find /var/www/ip2proxy-login \
-type f \
-exec chmod 644 {} \;Step 9: Understand the risk policy
For a low-risk connection, IP2Proxy data must be available, the connection must not be identified as a proxy, the Fraud Score must be below 50, no threat must be present, and the network must not be classified as DCH.
The result is:
For an elevated-risk connection, additional verification is required when a proxy is detected, Fraud Score is at least 50, the network is classified as DCH, or IP2Proxy information is unavailable.
The demo verification code is:
246810
A real implementation could replace this with TOTP, a passkey, email verification, device verification or another MFA mechanism.
For high risk, the demo blocks the login when Fraud Score is 80 or higher or a threat classification is present:
The thresholds in this tutorial are demonstration values, not universal recommended thresholds.
Step 10: Why check more than Is-Proxy?
It may seem sufficient to check:
$isProxy =
strcasecmp(
$_SERVER['HTTP_X_IP2PROXY_IS_PROXY'],
'True'
) === 0;$isProxy =
strcasecmp(
$_SERVER['HTTP_X_IP2PROXY_IS_PROXY'],
'True'
) === 0;However, the Caddy middleware treats certain infrastructure classifications differently when generating Is-Proxy.
For example:
DCH
represents data center or hosting infrastructure.
A result could therefore look like:
X-IP2Proxy-Is-Proxy: False
X-IP2Proxy-Proxy-Type: DCHX-IP2Proxy-Is-Proxy: False
X-IP2Proxy-Proxy-Type: DCHFor login protection, this is useful. A data center connection should not necessarily be called an anonymous proxy, but it can still be considered an elevated-risk login signal.
That is why our PHP application considers:
Is-Proxy
Proxy-Type
Threat
Fraud-Score
together.
Step 11: Configure Caddy
Open the Caddyfile:
sudo nano /etc/caddy/Caddyfilesudo nano /etc/caddy/CaddyfileReplace it with:
{
order ip2proxy before php_fastcgi
}
example.com {
root * /var/www/ip2proxy-login
# Remove any client-supplied IP2Proxy values.
request_header -X-IP2Proxy-*
# This tutorial assumes Caddy is directly exposed
# to the Internet.
#
# Remove forwarded client-IP headers so a client
# cannot choose which IP address IP2Proxy checks.
request_header -CF-Connecting-IP
request_header -X-Real-IP
request_header -X-Forwarded-For
request_header -Forwarded
ip2proxy {
mode local
bin_path /var/lib/ip2proxy/IP2PROXY-PX12.BIN
header_prefix X-IP2Proxy
}
php_fastcgi unix//run/php/php8.4-fpm.sock
file_server
}{
order ip2proxy before php_fastcgi
}
example.com {
root * /var/www/ip2proxy-login
# Remove any client-supplied IP2Proxy values.
request_header -X-IP2Proxy-*
# This tutorial assumes Caddy is directly exposed
# to the Internet.
#
# Remove forwarded client-IP headers so a client
# cannot choose which IP address IP2Proxy checks.
request_header -CF-Connecting-IP
request_header -X-Real-IP
request_header -X-Forwarded-For
request_header -Forwarded
ip2proxy {
mode local
bin_path /var/lib/ip2proxy/IP2PROXY-PX12.BIN
header_prefix X-IP2Proxy
}
php_fastcgi unix//run/php/php8.4-fpm.sock
file_server
}The key configuration is:
Caddyfile
ip2proxy {
mode local
bin_path /var/lib/ip2proxy/IP2PROXY-PX12.BIN
header_prefix X-IP2Proxy
}ip2proxy {
mode local
bin_path /var/lib/ip2proxy/IP2PROXY-PX12.BIN
header_prefix X-IP2Proxy
}The mode local instructs the middleware to query the local PX12 BIN database.
The following ordering is also important:
Caddyfile
{
order ip2proxy before php_fastcgi
}{
order ip2proxy before php_fastcgi
}It ensures IP2Proxy processes the request before PHP receives it.
Step 12: Prevent spoofed IP2Proxy headers
Because HTTP headers originate from the client, an attacker could attempt to send:
X-IP2Proxy-Is-Proxy: False
or:
X-IP2Proxy-Fraud-Score: 0
The Caddy configuration removes incoming IP2Proxy headers:
request_header -X-IP2Proxy-*
The intended flow is therefore:
Step 13: Prevent client-IP spoofing
The IP2Proxy middleware can inspect common forwarded client-IP headers before falling back to the remote connection address.
That behavior is useful when Caddy is behind a CDN or load balancer, but our demonstration assumes Caddy is directly Internet-facing.
Otherwise a client could attempt something like:
X-Forwarded-For: 8.8.8.8
Our Caddy configuration therefore removes:
request_header -CF-Connecting-IP
request_header -X-Real-IP
request_header -X-Forwarded-For
request_header -Forwarded
IP2Proxy will then use the actual connection address.
If Caddy is behind Cloudflare, another reverse proxy or a load balancer, you should instead establish a proper trusted-proxy configuration. Do not blindly remove the legitimate real-client-IP header in that deployment model.
Step 14: Validate the Caddyfile
Validate the configuration:
sudo caddy validate \
--config /etc/caddy/Caddyfile \
--adapter caddyfilesudo caddy validate \
--config /etc/caddy/Caddyfile \
--adapter caddyfile
Step 15: Restart Caddy
Restart the service:
sudo systemctl restart caddysudo systemctl restart caddyCheck status:
sudo systemctl status caddysudo systemctl status caddyIf something fails, check the log:
sudo journalctl \
-u caddy \
-n 100 \
--no-pagersudo journalctl \
-u caddy \
-n 100 \
--no-pagerStep 16: Open the demo
From your computer, visit your own domain:
E.g., https://example.com
The page should display the login form together with the IP2Proxy risk information.
Step 17: Test a normal connection
First access the server without intentionally using a VPN, Tor or proxy.
A normal connection could show:
IP2Proxy available: Yes
Proxy detected: False
Proxy type: โ
Threat: โ
Provider: โ
Fraud score: 0
Risk level: LOW
The actual result depends on your public IP and current PX12 data.
Use:
Username: demo
Password: DemoPass123!
A low-risk connection should immediately produce:
Login successful
Step 18: Test with a VPN
Connect your computer to a commercial VPN and reload the page.
A detected VPN may show:
Proxy detected: True
Proxy type: VPN
Provider: Example VPN Provider
Threat: โ
Fraud score: 35
Risk level: ELEVATED
Enter:
Username: demo
Password: DemoPass123!
Instead of completing authentication immediately, the site should display:
Additional verification required
Enter:
246810
The login should then succeed.
The flow has changed from:
to:
This is the main point of the demonstration: a VPN user is not automatically rejected, but the authentication requirements become stronger.
Step 19: Test the high-risk flow
A high-risk connection might look like:
Proxy detected: True
Proxy type: PUB
Threat: SPAM/SCANNER
Fraud score: 99
Risk level: HIGH
Submitting the correct username and password should produce:
Login blocked
and PHP returns:
HTTP 403
Step 20: What Caddy sends to PHP
The lookup happens before PHP is executed:
The middleware can provide headers such as:
X-IP2Proxy-Is-Proxy
X-IP2Proxy-Proxy-Type
X-IP2Proxy-Country-Code
X-IP2Proxy-Country-Name
X-IP2Proxy-Region
X-IP2Proxy-City
X-IP2Proxy-ISP
X-IP2Proxy-Domain
X-IP2Proxy-Usage-Type
X-IP2Proxy-ASN
X-IP2Proxy-AS
X-IP2Proxy-Last-Seen
X-IP2Proxy-Threat
X-IP2Proxy-Provider
X-IP2Proxy-Fraud-Score
PHP receives them through $_SERVER.
For example:
$_SERVER['HTTP_X_IP2PROXY_IS_PROXY'];
Proxy type:
$_SERVER['HTTP_X_IP2PROXY_PROXY_TYPE'];
Provider:
$_SERVER['HTTP_X_IP2PROXY_PROVIDER'];
Fraud Score:
$_SERVER['HTTP_X_IP2PROXY_FRAUD_SCORE'];
The PHP application therefore does not need to open the PX12 database itself.
Step 21: Handle unavailable IP2Proxy data
The application checks whether proxy intelligence exists:
index.php
$signalAvailable =
isset($_SERVER['HTTP_X_IP2PROXY_IS_PROXY']);$signalAvailable =
isset($_SERVER['HTTP_X_IP2PROXY_IS_PROXY']);If IP2Proxy data is missing, this demo requires additional verification rather than automatically considering the request safe:
$requiresVerification =
!$shouldBlock &&
(
!$signalAvailable ||
$isProxy ||
$fraudScore >= 50 ||
$isElevatedNetworkType
);$requiresVerification =
!$shouldBlock &&
(
!$signalAvailable ||
$isProxy ||
$fraudScore >= 50 ||
$isElevatedNetworkType
);The result is:
This avoids blindly failing open.
Step 22: Keep PX12 current
Proxy and VPN infrastructure changes constantly, so the commercial PX12 database should be updated regularly.
Keep the live database at:
/var/lib/ip2proxy/IP2PROXY-PX12.BIN
After replacing it, restore ownership:
sudo chown root:caddy /var/lib/ip2proxy/IP2PROXY-PX12.BINsudo chown root:caddy /var/lib/ip2proxy/IP2PROXY-PX12.BINRestore permissions:
sudo chmod 640 /var/lib/ip2proxy/IP2PROXY-PX12.BINsudo chmod 640 /var/lib/ip2proxy/IP2PROXY-PX12.BINThen restart Caddy:
sudo systemctl restart caddysudo systemctl restart caddyProduction considerations
IP2Proxy should be treated as one component of a broader authentication-risk strategy. A VPN connection does not automatically mean that a visitor is malicious, and a normal residential connection does not guarantee that a login is legitimate.
For example, a real authentication system could combine IP2Proxy signals with failed-login velocity, device recognition, account history, impossible travel detection, CAPTCHA, MFA, passkeys, compromised-password detection and behavioral signals.
The difference between these two decisions is important:
versus:
The second approach makes better use of the information available from PX12.
The 50 and 80 Fraud Score thresholds in this tutorial are only examples. Production thresholds should be tuned using your own traffic and risk tolerance.
The debug table should also be removed from a real login page. Fields such as Proxy Type, Provider, Threat and Fraud Score should normally remain server-side.
Conclusion
The IP2Proxy Caddy Middleware allows proxy intelligence to be added to an HTTP request before the application processes it.
With the commercial PX12 database, the application can consider VPNs, Tor, residential proxies, data center infrastructure, provider information, threat classifications and Fraud Score.
This same architecture can also be applied to account registration, password resets, checkout verification, API authentication, account recovery and administrative portals.
The key idea is to use IP2Proxy information as a risk signal, allowing an application to apply stronger authentication controls when required instead of automatically blocking every user who connects through a privacy or proxy network.