speedtest/backend/getIP_util.php
Matthew Kobayashi f1f48ae53e
fix: return client IPv6 address via cloudflared (#757)
* fix: return client IPv6 address via cloudflared

The cloudflared reverse proxy populates the X-Forwarded-For header for origin IPv4 addresses, however origin IPv6 addresses are added in a different header: Cf-Connecting-Ipv6. This updates the getIP.php mechanism to retrieve the value of this header and to prefer it over other client IP headers (in both cases only if the Cf-Connecting-Ipv6 header exists and is not empty).

* fix: Validate and normalise IP addresses from request headers

getClientIp() used HTTP_CF_CONNECTING_IPV6 and other headers verbatim, allowing malformed values to reach ISP lookups and the offline DB.

Add normalizeCandidateIp() helper that trims whitespace, extracts the first comma-separated token, and validates via filter_var(). Require FILTER_FLAG_IPV6 for the CF header and fall through to the next source on failure.

Written with assistance from OpenCode using Claude Opus 4.6.
2026-03-05 23:52:59 +01:00

55 lines
1.8 KiB
PHP
Executable file

<?php
/**
* Normalize and validate an IP address candidate from a request header.
*
* Trims whitespace, takes the first comma-separated token (for XFF-like
* headers that may contain a chain of addresses), and validates the result
* with filter_var().
*
* @param string $raw Raw header value.
* @param int $extraFlags Additional FILTER_FLAG_* flags (e.g. FILTER_FLAG_IPV6).
*
* @return string|false The validated IP string, or false on failure.
*/
function normalizeCandidateIp($raw, $extraFlags = 0)
{
$ip = trim($raw);
// For XFF-like values, take the first address before a comma.
if (($pos = strpos($ip, ',')) !== false) {
$ip = trim(substr($ip, 0, $pos));
}
if ($ip === '') {
return false;
}
return filter_var($ip, FILTER_VALIDATE_IP, $extraFlags);
}
/**
* @return string
*/
function getClientIp()
{
// Cloudflare IPv6 header — must be a valid IPv6 address.
if (!empty($_SERVER['HTTP_CF_CONNECTING_IPV6'])) {
$ip = normalizeCandidateIp($_SERVER['HTTP_CF_CONNECTING_IPV6'], FILTER_FLAG_IPV6);
if ($ip !== false) {
return preg_replace('/^::ffff:/', '', $ip);
}
}
// Other forwarding / proxy headers — accept any valid IP.
foreach (['HTTP_CLIENT_IP', 'HTTP_X_REAL_IP', 'HTTP_X_FORWARDED_FOR'] as $header) {
if (!empty($_SERVER[$header])) {
$ip = normalizeCandidateIp($_SERVER[$header]);
if ($ip !== false) {
return preg_replace('/^::ffff:/', '', $ip);
}
}
}
// Fallback: REMOTE_ADDR is set by the web server and is always a single IP.
$ip = normalizeCandidateIp($_SERVER['REMOTE_ADDR'] ?? '');
if ($ip !== false) {
return preg_replace('/^::ffff:/', '', $ip);
}
return $_SERVER['REMOTE_ADDR'] ?? '';
}