How to force region in Cloudflare Workers

Hello all,

I am attempting to build a system that interacts with an API that expects the API key to be geolocked to a certain region (The UK in my case). I have attempted to follow the methods explained by @user2765 but it seems Cloudflare has stopped this method from working.

Is there a new method to fulfill this, or should I be looking at other serverless function solutions.

Thank you for your time.

No, it still worked. Cloudflare are aware of this for several years and haven’t done anything to prevent it so I don’t think its something they want to stop. It’s possible by nature of their architecture, and could easily be stopped but they haven’t so I think its allowed, though I’m not sure.

It’s not something we use in production but I have re-tested it this morning and can confirm it works.

Setup an A record gb.yourdomain.com for the IP of one of Cloudflare’s United Kingdom datacenters, such as 8.41.7.4 which I confirmed working just now. You can use various IP tools to find all the IP’s for Cloudflare’s ASN in which location.

'strict mode';
export default {
  async fetch(request) {
    return fetch(request.url, {
      body: request.body,
      headers: request.headers,
      method: request.method,
      redirect: 'manual',
      cf: {
        resolveOverride: ['MAN','EDI','LHR'].includes(request.cf.colo)
          ? 'api.yourdomain.com'
          : 'gb.yourdomain.com'
      }
    });
  },
};

Make sure the :orange: Cloud is on for both your API record.

To explain whats happening here. Essentially the HTTP request hits Cloudflares IP for the hostname api.yourdomain.com which a Worker script is setup on. The worker then resolves the IP for API to instead be the IP of GB A Record which hits the datacenter in the desired geolocation and since it’s the same hostname (i.e. api.yourdomain.com) it invokes the same Worker script again but from another geolocation, it then hits the IP of your API server.

Here’s the test, for me I’m in China so the first request hits a datacenter in Hong Kong, and then the worker makes a second request to a datacenter in the United Kingdom. I will provide the logs for both requests below.

The first request:

{
  "outcome": "ok",
  "scriptName": "worker-weathered-glade-d581",
  "diagnosticsChannelEvents": [],
  "exceptions": [],
  "logs": [],
  "eventTimestamp": 1694839245034,
  "event": {
    "request": {
      "url": "https://api.yourdomain.com/",
      "method": "GET",
      "headers": {
        "accept": "*/*",
        "accept-encoding": "gzip",
        "cf-connecting-ip": "REDACTED",
        "cf-ipcountry": "CN",
        "cf-ray": "REDACTED",
        "cf-visitor": "{\"scheme\":\"https\"}",
        "connection": "Keep-Alive",
        "host": "api.yourdomain.com",
        "user-agent": "curl/8.1.2",
        "x-forwarded-proto": "https",
        "x-real-ip": "REDACTED"
      },
      "cf": {
        "clientTcpRtt": 76,
        "longitude": "104.05550",
        "latitude": "30.64980",
        "tlsCipher": "AEAD-AES256-GCM-SHA384",
        "continent": "AS",
        "asn": 9808,
        "country": "CN",
        "tlsClientAuth": {
          "certIssuerDNLegacy": "",
          "certIssuerSKI": "",
          "certSubjectDNRFC2253": "",
          "certSubjectDNLegacy": "",
          "certFingerprintSHA256": "",
          "certNotBefore": "",
          "certSKI": "",
          "certSerial": "",
          "certIssuerDN": "",
          "certVerified": "NONE",
          "certNotAfter": "",
          "certSubjectDN": "",
          "certPresented": "0",
          "certRevoked": "0",
          "certIssuerSerial": "",
          "certIssuerDNRFC2253": "",
          "certFingerprintSHA1": ""
        },
        "tlsExportedAuthenticator": {
          "clientFinished": "6f1b5d862487a8e52cd205c10e0a612872663f5beacac5af8d7a0e018d7fd143864fae0faca91f93a1904b01efb8345b",
          "clientHandshake": "21d13e08a59128ff45aca00b79c467151db3c200783a15cc71728283ae631494dfcf02b9e5e4776236132e2c6a7bf645",
          "serverHandshake": "c1f3740dcda5d22dc31257e52c66c24422c29b37c5c42a7bef419088b7230fe59764ff4b9a4d2c2792b9f20efd325dbf",
          "serverFinished": "bd114e5a9ba1ddd4e00132eefed2020942d88c80233f574a4874c7d1040c8b8964fd57e71363e22e57a66c4070eccca9"
        },
        "tlsVersion": "TLSv1.3",
        "city": "Chengdu",
        "timezone": "Asia/Shanghai",
        "region": "Sichuan",
        "requestPriority": "weight=16;exclusive=0;group=0;group-weight=0",
        "colo": "HKG",
        "httpProtocol": "HTTP/2",
        "regionCode": "SC",
        "asOrganization": "China Mobile",
        "edgeRequestKeepAliveStatus": 1
      }
    },
    "response": {
      "status": 200
    }
  },
  "id": 1
}

And the 2nd request (made by the Worker).

{
  "outcome": "ok",
  "scriptName": "worker-weathered-glade-d581",
  "diagnosticsChannelEvents": [],
  "exceptions": [],
  "logs": [],
  "eventTimestamp": 1694839486890,
  "event": {
    "request": {
      "url": "https://api.yourdomain.com/",
      "method": "GET",
      "headers": {
        "accept": "*/*",
        "accept-encoding": "gzip",
        "cf-connecting-ip": "162.158.178.8",
        "cf-ipcountry": "HK",
        "cf-ray": "REDACTED",
        "cf-visitor": "{\"scheme\":\"https\"}",
        "connection": "Keep-Alive",
        "host": "api.yourdomain.com",
        "user-agent": "curl/8.1.2",
        "x-forwarded-for": "REDACTED",
        "x-forwarded-proto": "https",
        "x-real-ip": "162.158.178.8"
      },
      "cf": {
        "clientTcpRtt": 198,
        "longitude": "114.17590",
        "latitude": "22.28420",
        "tlsCipher": "AEAD-AES128-GCM-SHA256",
        "continent": "AS",
        "asn": 13335,
        "clientAcceptEncoding": "gzip",
        "country": "HK",
        "tlsClientAuth": {
          "certIssuerDNLegacy": "",
          "certIssuerSKI": "",
          "certSubjectDNRFC2253": "",
          "certSubjectDNLegacy": "",
          "certFingerprintSHA256": "",
          "certNotBefore": "",
          "certSKI": "",
          "certSerial": "",
          "certIssuerDN": "",
          "certVerified": "NONE",
          "certNotAfter": "",
          "certSubjectDN": "",
          "certPresented": "0",
          "certRevoked": "0",
          "certIssuerSerial": "",
          "certIssuerDNRFC2253": "",
          "certFingerprintSHA1": ""
        },
        "tlsExportedAuthenticator": {
          "clientFinished": "002886b97a97b7ad5e6804419197e5d080c4b8c1da9af19f39603e08caf92403",
          "clientHandshake": "0aa5a5eb9cb79e1a0e61c08a4d32476f75e07eca2c7bfd47175a9daa85529e26",
          "serverHandshake": "69f056495c64bef187dfe8647da8385d37e7e8123001cc781cf088b8e8753718",
          "serverFinished": "2f1e2d2bb395132dfdd7fc0a65d08e9df2c333785f87353519d62872b90a08c4"
        },
        "tlsVersion": "TLSv1.3",
        "city": "Hong Kong",
        "timezone": "Asia/Hong_Kong",
        "region": "Central and Western District",
        "requestPriority": "weight=16;exclusive=0;group=0;group-weight=0",
        "colo": "MAN",
        "httpProtocol": "HTTP/2",
        "regionCode": "HCW",
        "asOrganization": "Cloudflare",
        "edgeRequestKeepAliveStatus": 1
      }
    },
    "response": {
      "status": 200
    }
  },
  "id": 0
}

Hope that helps. It might be nice if Cloudflare make an official guide rather than the community or developers having to figure this stuff out. It’s not easy for a beginner to just think of this stuff since they don’t have the big picture understanding of how everything works.

1 Like

Thanks for the help, still getting an error with the code 1042 (which I forgot to mention I was getting before).

Here is a simplified version of my code that just acts as a proxy:

async function proxy(request: Request): Promise<Response> {
	const body = (await request.json()) as Request;

	return fetch(body.url, {
		...body,
		cf: {
			resolveOverride: 'api.mydomain.com',
		},
	});
}

async function forceRegion(request: Request, env: Env): Promise<Response> {
	return fetch(request.url, {
		...request,
		redirect: 'manual',
		cf: {
			resolveOverride: 'gb.mydomain.com',
		},
	});
}

export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		return request.cf && ['MAN', 'EDI', 'LHR'].includes(request.cf.colo as string) ? proxy(request) : forceRegion(request, env);
	},
};

I have a Worker Route setup to redirect api.mydomain.com to my worker, and gb.mydomain.com is set to the IP address you provided above.

Forgot to add the ‘What am I missing?’ to the above reply haha

Update: I fiddled with some things and got it to throw a 1001 instead of a 1042.

Double check your DNS records. Make sure you’ve typed your domain correctly in the resolveOverride. Of course “mydomain.com” is just a placeholder.

I wonder if it has any relation to the worker compatibility_date?

@user2765 Was your test with the latest compatibility_date and a new project?

It was a fresh worker created this morning. I created the worker in the browser and then copy & paste the code into the in-browser editor of the Cloudflare Worker.

It works straight away, as it had for years past.

After fiddling with it some more I am happy to report that I have it now working for the UK. One thing I am having issues with is finding these WARP IPs for different Cloudflare Colos. What methods are you using to locate these IPs?

Can please share exactly what “fiddling” was required :sweat_smile:?

I must be missing something basic because I seem to have gotten all possible error codes apart from the desired result… :rofl:

For sure! Here some pretty stupid things I forgot:

  • Add a Worker Route to your WARP domain (gb.yourdomain.com) that points to the worker
  • Add a Worker Route to your API domain (api.yourdomain.com) that points to the worker
  • Make sure api.yourdomain.com has a proxied :orange: A record to 188.114.96.3 (I copied this from @user2765’s original post/video)
  • Make sure your gb.yourdomain.com A record is proxied :orange: and set to 8.41.7.4
  • Update the proxy() function to this to stop it from crashing the entire worker when there is an invalid JSON body
async function proxy(request: Request): Promise<Response> {
	let response: Response | undefined;

	try {
		const body = (await request.json()) as {
			url: string;
			options: RequestInit;
		};
		response = await fetch(body.url, {
			...body.options,
			cf: {
				resolveOverride: 'api.yourdomain.com',
			},
		});
	} catch (error: unknown) {
		response = new Response(
			JSON.stringify({
				error: error instanceof Error ? error.message : error,
				message: 'Error running worker',
			}),
			{
				status: 500,
				headers: {
					'Content-Type': 'application/json',
				},
			}
		);
	}

	return response;
}

Thanks.

Turns out using a subdomain from another zone/domain as the final fetch doesn’t work and the error doesn’t indicate that. Switching that with a subdomain from the same domain gets it working.

You can probably use the Geolite databases (Release 2023.09.16 ¡ P3TERX/GeoLite.mmdb ¡ GitHub) and filter by the ASN (13335) + Subnet (8.0.0.0/8) on ASN database to get the set of global subnets and then use the City database to narrow it down to a city or country.

You may need to ping the final list of subnets to get some usable IPs.

I’ve got a list of subnets but don’t have the know-how to figure out which IPs on the subnet are usable.

I tried using nmap -n -sP 8.41.7.0/24 while not connected to Cloudflare WARP and the command hung, while when I connect to Cloudflare WARP via their desktop app it says all hosts are available. Do I just pick any IP then?

You may want to try nmap via sudo. It fails silently when it doesn’t have the correct permissions for generating the ICMP packets.

Running the same command gives me:

sudo nmap -n -sP 8.41.7.0/2
Nmap done: 256 IP addresses (26 hosts up) scanned in 11.20 seconds

Thanks for the tip, I ran sudo nmap -n -sP 8.41.7.0/24 (I am assuming you forgot the 4 in your last reply, correct me if I am wrong) and got a result of Nmap done: 256 IP addresses (3 hosts up) scanned in 9.47 seconds. Is there a reason I am missing so many online IPs compared to you?

Yeah, the trailing 4 got missed but not sure how to explain the difference in the number of hosts. I did try the same command from different servers and all gave me the same 26 hosts.

1 Like

Turns out it was because I was still connected to WARP. Working great now! Thanks a bunch @darthShadow and @user2765.

1 Like

This topic was automatically closed 3 days after the last reply. New replies are no longer allowed.