Skip to content

Cloudflare Workers proxy on Rybbit Cloud geolocates visitors as United States #1034

Description

@mavam

Summary

When using the official Cloudflare Workers proxy pattern with Rybbit Cloud, pageviews from a German visitor are attributed to the United States.

The browser is seen by Cloudflare as Germany, and the Worker forwards the original visitor IP via X-Forwarded-For, but Rybbit appears to geolocate Cloudflare's synthetic Worker subrequest IP (2a06:98c0:3600::103) instead of the forwarded visitor IP.

This seems specific to the topology visitor -> Cloudflare Worker on customer zone -> app.rybbit.io, where app.rybbit.io is also behind Cloudflare.

Related: #444

Setup

  • Rybbit Cloud: https://app.rybbit.io
  • Website served from Cloudflare Workers static assets
  • Tracker loaded as first-party script:
<script src="/analytics/script.js" data-site-id="..." defer></script>
  • Worker proxy maps /analytics/* to https://app.rybbit.io/api/*
  • Worker sets X-Forwarded-For and X-Real-IP to the incoming CF-Connecting-IP
  • Worker does not forward cookies or auth headers

This follows the same core pattern as the Rybbit Cloudflare Workers proxy guide:

https://github.com/rybbit-io/rybbit/blob/master/docs/content/docs/(docs)/proxy-guide/cloudflare-workers.mdx

The Astro proxy guide is also relevant for the first-party proxy model:

https://github.com/rybbit-io/rybbit/blob/master/docs/content/docs/(docs)/proxy-guide/astro.mdx

What I observed

From the same Safari session that produced the Rybbit visit, Cloudflare reports Germany:

https://www.cloudflare.com/cdn-cgi/trace
colo=DUS
loc=DE
warp=off
gateway=off
rbi=off

But the Rybbit dashboard shows the session country as United States.

I also checked the Rybbit browser script behavior. Loading /analytics/script.js causes the script to derive the analytics host from its own script URL, so events are posted to /analytics/track. They are not bypassing the proxy and going directly from the browser to https://app.rybbit.io/api/track.

Edge behavior

Using wrangler dev --remote with a minimal Worker that fetches https://www.cloudflare.com/cdn-cgi/trace while setting X-Forwarded-For and X-Real-IP to the visitor IP, the target Cloudflare zone still sees:

ip=2a06:98c0:3600::103
colo=DUS
loc=DE

This matches Cloudflare's documented cross-zone Worker behavior:

https://developers.cloudflare.com/fundamentals/reference/http-headers/#cf-connecting-ip

Cloudflare documents that cross-zone Worker subrequests set CF-Connecting-IP to 2a06:98c0:3600::103 for security. Public GeoIP data maps that synthetic IP to the United States, which matches the Rybbit dashboard symptom.

Why this looks like an ingest-side issue

Current master has resolveClientIp() with ASN-aware proxy detection:

https://github.com/rybbit-io/rybbit/blob/master/server/src/services/tracker/resolveClientIp.ts

If CF-Connecting-IP is classified as a datacenter or proxy edge, Rybbit should use X-Real-IP or the first X-Forwarded-For entry. However, the code also documents a fallback where, if the ASN DB is unavailable or the edge IP is not classified, Rybbit treats the request as direct and CF-Connecting-IP wins. In this topology that means geolocating 2a06:98c0:3600::103.

Cloudflare's synthetic Worker IP currently resolves to AS132892. That ASN appears in the generated datacenter ASN list, so this should work if the ASN DB is available and the resolver running in Rybbit Cloud is current.

Expected behavior

For first-party Cloudflare Workers proxy setups following the Rybbit docs, Rybbit Cloud should attribute location to the original visitor from the forwarded visitor IP, not to Cloudflare's synthetic Worker subrequest IP.

In practice, that means trusting the first X-Forwarded-For entry when the request is known to have come through a Cloudflare Worker proxy.

Possible fixes

  • Special-case CF-Connecting-IP: 2a06:98c0:3600::103 as a proxied edge and trust X-Forwarded-For.
  • Special-case requests with CF-Worker present as proxied when X-Forwarded-For is present.
  • Make the ASN DB missing or unknown fallback configurable for Rybbit Cloud so documented first-party proxy setups do not silently regress to the proxy IP.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions