Python REST API'lerde Gerçek İstemci IP'sini Almak
Selamlar, bu yazımızda Python ile yazdığımız REST API'lerde istemcinin gerçek IP adresini nasıl alırız, ona bakacağız. Konu bir bakışta basit görünür: 'request nesnesinden remote_addr'i çek, bitti'. Ama işin içine Nginx, ALB, Cloudflare girince iş değişiyor. Hadi başlayalım.
İstemci IP'sini bilmek; rate limiting, fraud tespiti, audit log ve coğrafi yönlendirme gibi pek çok yerde işimize yarar. Yanlış aldığınızda ise tüm bu sistemler ya kullanıcıyı haksız yere engeller ya da kötü niyetli birine kapıyı sonuna kadar açar. Bu yüzden 'çalışıyor gibi duruyor' yetmiyor, doğru çalışması gerekiyor.
Doğrudan bağlantı: en basit hâli
Eğer uygulamanız önünde hiçbir şey olmadan, açık bir socket'le çalışıyorsa Flask tarafında işiniz kolay. request.remote_addr size TCP bağlantısının diğer ucundaki IP'yi verir.
from flask import Flask, request
app = Flask(__name__)
@app.route('/whoami')
def whoami():
# Ters proxy yoksa remote_addr dogrudur
return {'client_ip': request.remote_addr}
Bu senaryo lab ortamında ya da intranet servisinde geçerli. Production'da çoğumuz uygulamayı bir Nginx, Traefik ya da bulut yük dengeleyicinin arkasına koyuyoruz. O zaman remote_addr size proxy'nin IP'sini söyler, gerçek kullanıcının değil.
Ters proxy arkasında: ProxyFix devreye giriyor
Werkzeug'un ProxyFix middleware'i tam bu iş için var. Önündeki proxy'lerden geleni güvenli bir şekilde sarmalayıp WSGI ortamına yansıtıyor. Kritik nokta: kaç hop güvendiğinizi açıkça belirtmeniz gerekiyor.
from flask import Flask, request
from werkzeug.middleware.proxy_fix import ProxyFix
app = Flask(__name__)
# Onumuzde tek bir guvenilir proxy var
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1)
@app.route('/whoami')
def whoami():
# Artik remote_addr orijinal istemciyi gosteriyor
return {'client_ip': request.remote_addr}
x_for=1 diyorsanız, X-Forwarded-For zincirinin sağdan birinci elemanını alır. İki proxy varsa (mesela Cloudflare + ALB), x_for=2 yapacaksınız. Sayıyı yanlış verirseniz spoofing kapısı aralanır - aşağıda buna döneceğim.
Elle X-Forwarded-For ayrıştırmak
Daha kontrollü olmak isteyenler için manuel yol da var. Ben şahsen küçük servislerde ProxyFix'i tercih ederim ama trafik karmaşık olunca açıkça yazmak okunurluğu artırıyor.
from flask import Flask, request
import ipaddress
app = Flask(__name__)
GUVENILIR_AGLAR = [
ipaddress.IPv4Network('10.0.0.0/8'),
ipaddress.IPv4Network('172.16.0.0/12'),
ipaddress.IPv4Network('192.168.0.0/16'),
ipaddress.IPv4Network('127.0.0.0/8'),
]
def guvenilir_proxy_mi(ip: str) -> bool:
try:
adres = ipaddress.IPv4Address(ip)
return any(adres in ag for ag in GUVENILIR_AGLAR)
except ValueError:
return False
def gercek_ip() -> str:
xff = request.headers.get('X-Forwarded-For', '')
if xff and guvenilir_proxy_mi(request.remote_addr):
# En soldaki, zincirin basindaki orijinal istemcidir
return xff.split(',')[0].strip()
return request.remote_addr
Burada kilit şu: önce remote_addr'in güvendiğiniz bir aralıkta olduğunu doğruluyoruz, sonra X-Forwarded-For'a bakıyoruz. Aksi takdirde dışarıdan gelen biri kendi isteğine başlık ekleyip 'ben aslında 8.8.8.8'im' diyebilir.
FastAPI tarafı
FastAPI'de request.client.host doğrudan bağlantının IP'sini verir. Forwarded başlık mantığı aynı kalıyor, sadece sözdizimi değişiyor.
from fastapi import FastAPI, Request
app = FastAPI()
@app.middleware('http')
async def gercek_ip_middleware(request: Request, call_next):
xff = request.headers.get('x-forwarded-for')
if xff:
request.state.client_ip = xff.split(',')[0].strip()
else:
request.state.client_ip = request.client.host
return await call_next(request)
@app.get('/whoami')
async def whoami(request: Request):
return {'client_ip': request.state.client_ip}
Production'da bu middleware'in başına da bir 'gerçekten proxy'den mi geliyor' kontrolü koymak şart. Aksi takdirde Flask'taki tuzağın aynısı sizi bulur.
Sık karşılaşılan hatalar
- X-Forwarded-For'u körlemesine okumak: Doğrudan bağlantıda gelen istek bu başlığı kendi yazabilir. Önce
remote_addr'in güvenilir bir proxy IP'si olduğunu doğrulayın. - Hop sayısını yanlış vermek:
ProxyFix(x_for=1)dediniz ama önünüzde Cloudflare + ALB var. Saldırgan kendi başlığına istediği IP'yi koyup 'orijinal' olarak görünebilir. Altyapıda kaç proxy varsa o sayıyı verin. - En sağdaki IP'yi almak: X-Forwarded-For zinciri soldan başlar, en soldaki orijinal istemcidir. Sağdaki en son ekleyen proxy.
- IPv6'yı unutmak: Bu yazıda IPv4 üzerinden gittik ama production'da
ipaddress.ip_addressveip_networkkullanmak iki ailesini de kapsar. - Log ve rate limiter'ı farklı IP'lerle beslemek: Audit log'a
remote_addr, rate limiter'agercek_ip()koyarsanız korelasyon kâbusa döner. Tek bir kaynaktan dağıtın.
Doğrulama
Yerelde bir uçtan uca testi şöyle yapabilirsiniz. Önce uygulamayı 0.0.0.0:8000'de çalıştırın, sonra:
# Dogrudan baglanti
curl -s http://localhost:8000/whoami
# Sahte X-Forwarded-For (kotu niyetli istek simulasyonu)
curl -s -H 'X-Forwarded-For: 8.8.8.8' http://localhost:8000/whoami
İkinci komut size hâlâ 127.0.0.1 döndürüyorsa kontrolünüz çalışıyor. 8.8.8.8 döndürüyorsa, doğrulama mantığı eksik demektir.
Kapanış
Özetle: X-Forwarded-For'a güvenmek için önce kimden geldiğine güvenmek lazım. Bence en sağlıklı yol, Flask'ta ProxyFix ile hop sayısını net belirlemek; FastAPI'de ise middleware içinde aynı doğrulamayı elle yapmak. Altyapı değiştiğinde - yeni bir CDN, ek bir LB - bu ayarı güncellemeyi unutmayın, çünkü sessizce kırılır. Umarım faydalı olur, bir sonraki yazıda görüşmek üzere.
