Testing a France-only geo redirect without a French IP

这篇内容暂时只有 English 版本,以下是原文。

We run a small storefront for French customers on a single VPS in Roubaix (OVH, Debian 12, nginx 1.24). Last month we added a country redirect: visitors from outside France should land on /en/, French visitors on /. The rule was two lines of nginx geo plus a return 302. It worked from my laptop in Lille and from a colleague in Lyon. It also worked from a VPN exit in Frankfurt, which is exactly what fooled us.

The first real signal came from a customer in Geneva. He was getting the French page, which was fine, but his colleague 20 km away on the same corporate network was getting /en/. Same company, same NAT, different result. That told me the problem was not the country lookup itself but which address nginx was looking at.

Here is what we had:

geo $country {
    default        FR;
    include        /etc/nginx/geo/not-fr.conf;
}

map $country $lang_prefix {
    FR             "";
    default        "/en";
}

server {
    listen 443 ssl http2;
    server_name example.fr;

    if ($lang_prefix != "") {
        return 302 $scheme://$host$lang_prefix$request_uri;
    }
}

And the geo file was generated weekly from a MaxMind CSV, one CIDR per line, 203.0.113.0/24 FR; style. The generator wrote far fewer entries than we expected: about 180k lines, which sounds like a lot until you remember that a country file with only non-FR ranges is not the same as a full country database. We had inverted the logic in the generator and never noticed, because every test we ran was from an address that happened to be in the file.

The second problem was the corporate NAT. geo matches on $remote_addr, which is the TCP peer. Behind a load balancer or a CDN, that is the balancer, not the user. We had real_ip configured for our own LB, but the Geneva customer was arriving through his company’s egress proxy, so $remote_addr was a French-looking address owned by the proxy provider. There is no fix for that on our side; it is a property of the visitor’s network, not our config.

We rewrote the generator to emit the full FR set, then test the negative case explicitly. The check we now run before every deploy:

# should be empty
curl -sI https://example.fr/ | grep -i '^location:'
# should point at /en/
curl -sI -H 'X-Forwarded-For: 198.51.100.7' https://example.fr/ | grep -i '^location:'

The second command only works because we trust the header from our own LB, and only for the private ranges we listed in set_real_ip_from. If you trust X-Forwarded-For from the open internet, any visitor can pick their language by sending a header, which is a mild but real cache-poisoning risk if you put a shared cache in front.

Where this still breaks: IPv6 ranges were missing from our first regenerated file because the CSV export defaulted to IPv4 only; mobile carriers that hand out addresses registered in another country; and anyone on a VPN, which is a growing share of our traffic and which we have decided not to fight. We also gave up on the redirect for the API host entirely, because a 302 on a JSON endpoint confused one client’s retry logic for a week.

The redirect is now a soft suggestion: we set a cookie and let the app decide, with the nginx rule only as a first-visit fallback. That costs one extra request on the first visit and removes the whole class of “wrong country” complaints from people whose address is not where they are.

If you run a similar country rule, what do you do about visitors behind a corporate proxy in a neighbouring country? We are considering dropping geo entirely for logged-in users and only using it on the marketing pages. I would like to hear from anyone who has measured how much of their French traffic actually arrives with a non-French $remote_addr.

0
赞同度 100% · 0 条评论 · 5 阅读

0 条评论

支持 Markdown · 回复会实时推送给对方

还没有评论,写下第一条。