Skip to content

fix: return client IPv6 address via cloudflared#757

Merged
sstidl merged 2 commits intolibrespeed:masterfrom
MattKobayashi:fix/cloudflared-ipv6
Mar 5, 2026
Merged

fix: return client IPv6 address via cloudflared#757
sstidl merged 2 commits intolibrespeed:masterfrom
MattKobayashi:fix/cloudflared-ipv6

Conversation

@MattKobayashi
Copy link
Copy Markdown
Contributor

@MattKobayashi MattKobayashi commented Mar 5, 2026

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).

A (sanitised) example of request headers passed by cloudflared:

{
  "headers": {
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
    "Accept-Encoding": "gzip, br",
    "Accept-Language": "en-AU,en;q=0.9",
    "Cdn-Loop": "cloudflare; loops=1",
    "Cf-Connecting-Ip": "254.252.239.103",
    "Cf-Connecting-Ipv6": "2403:580c:<redacted>",
    "Cf-Ipcountry": "AU",
    "Cf-Pseudo-Ipv4": "254.252.239.103",
    "Cf-Ray": "9d754acd5f8bd71b-BNE",
    "Cf-Visitor": "{\"scheme\":\"https\"}",
    "Cf-Warp-Tag-Id": "<redacted>",
    "Connection": "keep-alive",
    "Host": "<redacted>",
    "Priority": "u=0, i",
    "Sec-Fetch-Dest": "document",
    "Sec-Fetch-Mode": "navigate",
    "Sec-Fetch-Site": "none",
    "Upgrade-Insecure-Requests": "1",
    "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.3 Safari/605.1.15",
    "X-Forwarded-For": "254.252.239.103",
    "X-Forwarded-Proto": "https"
  }
}

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).
@qodo-free-for-open-source-projects
Copy link
Copy Markdown
Contributor

Review Summary by Qodo

Prioritize Cf-Connecting-Ipv6 header for IPv6 detection

🐞 Bug fix

Grey Divider

Walkthroughs

Description
• Prioritize Cf-Connecting-Ipv6 header for IPv6 client detection
• Cloudflared populates IPv6 in separate header, not X-Forwarded-For
• Reorder IP detection logic to check IPv6 header first
Diagram
flowchart LR
  A["Client Request"] -->|"Cloudflared Proxy"| B["Check Cf-Connecting-Ipv6"]
  B -->|"IPv6 Found"| C["Return IPv6 Address"]
  B -->|"Not Found"| D["Check HTTP_CLIENT_IP"]
  D -->|"Not Found"| E["Check HTTP_X_REAL_IP"]
  E -->|"Not Found"| F["Check HTTP_X_FORWARDED_FOR"]
Loading

Grey Divider

File Changes

1. backend/getIP_util.php 🐞 Bug fix +3/-1

Add IPv6 header detection with highest priority

• Added check for HTTP_CF_CONNECTING_IPV6 header as first priority
• Moved IPv6 header detection before existing IP detection methods
• Maintains fallback chain for other IP headers if IPv6 not present

backend/getIP_util.php


Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects
Copy link
Copy Markdown
Contributor

qodo-free-for-open-source-projects bot commented Mar 5, 2026

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Validate CF IPv6 header🐞 Bug ⛯ Reliability
Description
getClientIp() now returns HTTP_CF_CONNECTING_IPV6 verbatim when present; if it’s
malformed/contains extra data, downstream code that assumes a real IP will produce incorrect results
(failed ISP lookup, wrong local/private detection, polluted telemetry). Add trimming + IP validation
and only use this header when it’s a valid IPv6 address (otherwise fall back to other sources).
Code

backend/getIP_util.php[R7-10]

+    if (!empty($_SERVER['HTTP_CF_CONNECTING_IPV6'])) {
+        $ip = $_SERVER['HTTP_CF_CONNECTING_IPV6'];
+    } elseif (!empty($_SERVER['HTTP_CLIENT_IP'])) {
       $ip = $_SERVER['HTTP_CLIENT_IP'];
Evidence
The new change makes HTTP_CF_CONNECTING_IPV6 the highest-precedence source of truth for the client
IP, but the value is not normalized/validated before being used in URL construction and GeoIP
lookups that clearly expect an IP string.

backend/getIP_util.php[6-20]
backend/getIP.php[53-65]
backend/getIP.php[140-147]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`getClientIp()` now prefers `HTTP_CF_CONNECTING_IPV6` but uses it verbatim. If this header contains non-IPv6 content (malformed value, extra data, whitespace), downstream lookups (ipinfo URL path, offline DB lookup) will operate on an invalid &amp;quot;IP&amp;quot; string.
## Issue Context
The returned `$ip` is used for ISP lookup and displayed/stored; the code path assumes the value is a real IP address.
## Fix Focus Areas
- backend/getIP_util.php[6-20]
- backend/getIP.php[53-65]
- backend/getIP.php[140-147]
## Implementation notes
- Add a small helper to normalize candidate IPs: `trim`, for XFF-like values take first token before comma, then validate with `filter_var`.
- For `HTTP_CF_CONNECTING_IPV6`, require `FILTER_FLAG_IPV6`; if invalid, fall back to other headers.
- Keep the existing `::ffff:` stripping after validation/normalization.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Unconditional CF header trust🐞 Bug ⛨ Security
Description
HTTP_CF_CONNECTING_IPV6 is now trusted with highest precedence without verifying that the request
actually came through a trusted Cloudflare/cloudflared hop. If the origin can be reached directly
(or a proxy doesn’t strip this header), clients can inject the header and spoof the “client IP”
returned to the UI and saved in telemetry.
Code

backend/getIP_util.php[R7-9]

+    if (!empty($_SERVER['HTTP_CF_CONNECTING_IPV6'])) {
+        $ip = $_SERVER['HTTP_CF_CONNECTING_IPV6'];
+    } elseif (!empty($_SERVER['HTTP_CLIENT_IP'])) {
Evidence
The new precedence means the application accepts a client-controlled header as the canonical client
IP and then propagates it to user-visible output and database telemetry, with no guard that the
header is trustworthy in the current deployment topology.

backend/getIP_util.php[6-12]
backend/getIP.php[176-198]
results/telemetry.php[5-9]
results/telemetry.php[38-41]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`HTTP_CF_CONNECTING_IPV6` is now the top-precedence client IP source, but it’s trusted unconditionally. In deployments where the origin is reachable without the trusted proxy, this enables trivial header injection and IP spoofing.
## Issue Context
The chosen IP is returned to clients (`getIP.php`) and persisted to telemetry (`results/telemetry.php`), so spoofing affects both user-visible results and stored data integrity.
## Fix Focus Areas
- backend/getIP_util.php[6-20]
- backend/getIP.php[176-198]
- results/telemetry.php[5-9]
## Implementation notes
- Introduce a configuration switch (e.g. `TRUST_CLOUDFLARE_HEADERS`) or a trusted proxy IP allowlist.
- Only read `HTTP_CF_CONNECTING_IPV6` when the request is confirmed to be from that trusted proxy/tunnel; otherwise skip it.
- Combine with strict IPv6 validation (see other finding) to avoid accepting malformed values even from trusted hops.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

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.
@sstidl sstidl merged commit f1f48ae into librespeed:master Mar 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants