Как обнаружить VPN-пользователей на вашем сайте
Практическое руководство без привязки к вендору для команд антифрода, ad-ops, поддержки и платформенных инженеров. Готовые примеры на Node.js, Python, Go, PHP и Cloudflare Workers. SDK не нужен, API бесплатный и не требует аутентификации.
Что на самом деле означает обнаружение VPN
«Этот IP, VPN?» звучит как yes/no вопрос, но на практике вы получите бакет: clean, suspicious (дата-центр), vpn_likely или vpn_detected. Правильное продуктовое действие зависит от бакета, не от единого булева флага.
- vpn_detected — подтверждённый VPN-выход (multi-source совпадение). Блокируйте записи, step-up auth.
- vpn_likely, сильные сигналы, но без ground-truth подтверждения. Считайте повышенным риском.
- suspicious, дата-центр / хостинг IP. Почти никогда не реальный потребитель; может быть скрапер, автоматизированный браузер или self-hosted VPN.
- clean, резидентный или доверенный IP. Никаких сигналов обнаружения не сработало.
Серверная проверка (рекомендуется)
Вызовите POST /v1/check из вашего бэкенда с IP посетителя из reverse-proxy заголовков. Кэшируйте вердикт на 5 минут по IP.
Node.js (TypeScript)
async function checkVpn(ip: string) {
const r = await fetch("https://iplogs.com/v1/check", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ ip }),
});
const data = await r.json();
return {
verdict: data.verdict, // clean | suspicious | vpn_likely | vpn_detected
isVpn: data.is_vpn,
score: data.score, // 0–1
provider: data.ip_info?.vpn_provider ?? null,
sources: data.ip_info?.vpn_provider_sources ?? [],
};
}Python
import httpx
def check_vpn(ip: str) -> dict:
r = httpx.post(
"https://iplogs.com/v1/check",
json={"ip": ip},
timeout=5.0,
)
r.raise_for_status()
return r.json() # verdict, score, is_vpn, ip_info, signals[]Go
type Verdict struct {
Verdict string `json:"verdict"`
Score float64 `json:"score"`
IsVPN bool `json:"is_vpn"`
}
func CheckVPN(ctx context.Context, ip string) (*Verdict, error) {
body, _ := json.Marshal(map[string]string{"ip": ip})
req, _ := http.NewRequestWithContext(ctx, "POST",
"https://iplogs.com/v1/check", bytes.NewReader(body))
req.Header.Set("content-type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
defer resp.Body.Close()
var v Verdict
return &v, json.NewDecoder(resp.Body).Decode(&v)
}PHP
function checkVpn(string $ip): array {
$ctx = stream_context_create([
"http" => [
"method" => "POST",
"header" => "content-type: application/json",
"content" => json_encode(["ip" => $ip]),
"timeout" => 5,
],
]);
$body = file_get_contents("https://iplogs.com/v1/check", false, $ctx);
return json_decode($body, true);
}Edge-проверка (Cloudflare Worker)
Запустите проверку на edge до того, как запросы попадут на ваш origin. Удобно для блокировки VPN на маршрутах регистрации или логина без модификации кода приложения.
export default {
async fetch(req: Request) {
const ip = req.headers.get("cf-connecting-ip") ?? "";
const r = await fetch("https://iplogs.com/v1/check", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ ip }),
});
const { verdict } = await r.json();
if (verdict === "vpn_detected" && req.method !== "GET") {
return new Response("VPN-трафик заблокирован на этом маршруте", { status: 403 });
}
return fetch(req);
},
};Какие сигналы взвешивать в собственной модели риска
Не доверяйте только булеву флагу. Ответ IPLogs включает массив signals[], каждый сигнал, который сработал, с весом. Пример собственного скоринга:
function customRisk(detection) {
let risk = detection.score;
// Boost для подтверждённого коммерческого VPN
if (detection.ip_info?.vpn_provider) risk += 0.1;
// Boost для любого threat-intel hit
if (detection.signals?.some(s => s.type === "threat_intel_listed" && s.matched))
risk += 0.2;
// Discount для trusted-infra IP (DNS-резолверы, CDN edge)
if (detection.signals?.some(s => s.type === "asn_trusted_infra" && s.matched))
risk = 0.0;
return Math.min(1, risk);
}Типичные ошибки
- Не доверяйте IP от клиента. Всегда читайте из proxy-заголовка (
X-Forwarded-For,CF-Connecting-IP,X-Real-IP), никогда из JavaScript или request body. - Кэшируйте агрессивно. 5-минутный кэш по IP снижает API-нагрузку в 100 раз и срезает ~150 мс с критического пути.
- Apple Private Relay, отдельная категория. Многие из ваших реальных клиентов используют его по умолчанию. Жёсткая блокировка стоит вам регистраций.
- Не показывайте вердикт пользователю. «Мы заблокировали вас, потому что вы на VPN» точно говорит атакующему, какой сигнал избегать. Используйте generic-сообщение.
Часто задаваемые вопросы
+Какой самый надёжный способ обнаружения VPN-пользователей?
Подтверждение из нескольких источников. Один фид всегда дрейфует; ground-truth импорт из API самого VPN-провайдера плюс активная проверка протоколов устраняют ~99% ложных срабатываний. Решения только на основе агрегаторов (один CIDR-фид) пропускают новые exit-узлы и ошибочно помечают CGNAT.
+Стоит ли блокировать VPN-пользователей полностью?
Почти никогда. Большинство потребительского VPN-трафика легитимно (приватность, обход геоблокировок, недоверенные сети). Блокируйте на high-friction поверхностях (регистрация, оплата, сброс пароля) и step-up auth на платежах. Жёсткая блокировка чтения теряет лояльных пользователей.
+Замедлит ли обнаружение VPN мои запросы?
Не замедлит, если делать правильно. Кэшируйте вердикты на 5 минут по IP (Redis или in-memory), медианный запрос добавляет <1 мс. Сам IPLogs API возвращает sub-2 секунды на холодный IP и <1 мс на тёплый.
+Нужно ли обрабатывать iCloud Private Relay и Cloudflare WARP отдельно?
Да. Оба «анонимизирующие», но ни один не является коммерческим VPN, и многие из ваших самых лояльных пользователей по умолчанию используют Apple Private Relay. Обрабатывайте их как отдельный тип сигнала ("private_relay") и применяйте step-up только в комбинации с другими факторами риска.
После интеграции обнаружения смотрите готовые рецепты блокировки для Cloudflare WAF, Nginx и Stripe Radar. Или прочитайте полный справочник API.