3D Secure Geçti, Ödeme Geçmedi: Sanal POS IPv6'yı Tanımıyor
IPv6 ile gelen müşteri 3D Secure'u geçiyor, provizyonda "CustomerIPAddress" hatasıyla düşüyor. Banka "IPv4 gönderin" diyor, dokümanı kendi içinde çelişiyor. Çözüm bankada değil DNS'teydi: tarayıcıya IPv6 seçme fırsatı vermemek.
Bu yazının İngilizcesi de var →
Yeni çipli kimlik kartınızla bankaya gittiğinizi düşünün. Kapıdaki güvenlik kartı okutuyor, sizi içeri alıyor. Gişedeki memur ise formdaki kutucuklara bakıp "bu numara buraya sığmıyor, eski nüfus cüzdanınızı getirin" diyor. Eski cüzdanınız yok, çoktan değiştirdiniz.
Geçen hafta sonu bir e-ticaret müşterimizde yaşadığımız sanal POS IPv6 sorunu tam olarak buydu. Müşteri 3D Secure'u geçiyor, SMS şifresini giriyor, sipariş son adımda düşüyor. Hata nerede doğuyor, banka ne diyor, doküman aslında ne diyor, sırayla bakacağız. En utandırıcı kısmı da saklamayayım: o gün en büyük akıl tutulması bankada değil, bizim e-postamızdaydı.
Belirti: 3D Secure Geçiyor, Provizyon Düşüyor#
Site Cloudflare arkasında, ödeme Garanti BBVA sanal POS'u üzerinden. IPv4 ile gelen müşterilerde sorun yok. IPv6 ile gelen müşteride akış şöyle ilerliyor: 3D Secure sayfası açılıyor, SMS onayı geçiyor, banka başarılı doğrulama dönüyor. Ardından sunucumuz satış isteğini (provizyon) gönderiyor ve şu cevap geliyor:
respCode : 92
respText : Declined
Source : GVPS
ReasonCode : 0003
ErrorMsg : Giriş yaptığınız değerleri kontrol ediniz
SysErrMsg : CustomerIPAddress field must contain a valid ip address
İlk şüphe kendimizeydi: IP'yi iki adımda farklı mı tespit ediyoruz? Değil. Uygulama her adımda adresi yeniden okuyor ve iki değer birebir aynı. Aynı IPv6 adresi 3D adımında sorun çıkarmıyor, provizyonda "geçerli bir IP değil" sayılıyor.
Bankanın Cevabı: "IPv4 Gönderin"#
17 Ekim Cumartesi akşamı destek ekibine yazdık: hata kaydı, terminal bilgisi, ve IPv6 desteği olup olmadığı, yoksa önerilen yöntemin ne olduğu. Pazartesi sabah cevap geldi: IPv6 desteklenmiyor, işlemleri IPv4 formatında gönderin.
İkinci e-postada soruyu açık açık sorduk: IPv4 adresi olmayan müşterinin gerçek IP'sini IPv4 formatında nasıl gönderelim? Alan boş bırakılabilir mi? Bir geliştirme takvimi var mı? Aynı gün gelen ikinci cevap ilkinin genişletilmiş hâliydi: IPv6 formatı desteklenmiyor, şu an için böyle bir çalışma yok, onay için IPv4 gerekiyor.
Yani sorunun kendisi, çözüm olarak geri geldi. Kimlik kartı örneğine dönersek: "Eski cüzdanım yok" dediniz, memur "eski cüzdanınızı getirin" dedi.
Doküman Ne Diyor? İki Sayfa, İki Kural#
Destek ekibinin cevabını bir kenara koyup Garanti BBVA'nın geliştirici portalına baktım. Aynı müşteri IP'si için iki ayrı sayfada iki ayrı tarif var:
| Doküman | Alan | Uzunluk | Biçim kuralı |
|---|---|---|---|
| XML sanal POS, ek alanlar | Customer/IPAddress |
20 byte | "No format requirement" |
| 3D ödeme | customerIp |
en fazla 20 karakter | "It must be in ipv4 format." |
XML dokümanı alanın fraud kontrolleri için zorunlu olduğunu da yazıyor. Biçim konusunda ise sessiz; hatta açıkça "biçim şartı yok" diyor. Bizim yaşadığımız da tam bu ikilik: bir aşama adresi kabul ediyor, diğeri reddediyor.
20 Karakterlik Kutu#
IPv4 adresi en uzun hâliyle 15 karakter: 255.255.255.255. IPv6 adresi sekiz grup, her grup dört onaltılık hane, aralarında yedi iki nokta: 39 karakter. 20 karakterlik bir alan IPv4 düşünülerek açılmış ve IPv6 eklenirken genişletilmemiş.
Yalnız bu tek başına açıklama değil. Kısaltılmış IPv6 adresleri (2001:db8::1 gibi) 20 karaktere rahatça sığıyor. Hata mesajı da "çok uzun" demiyor, "geçerli bir IP değil" diyor. Yani provizyon tarafında uzunluk kontrolünden önce bir IPv4 doğrulayıcı çalışıyor.
Bağlam için bir rakam: Internet Society'nin aktardığı Google verisine göre 28 Mart 2026'da Google'a erişimin yüzde 50,10'u IPv6 üzerinden yapıldı. İlk kez yarıyı geçti. Gişedeki form ise hâlâ eski cüzdana göre basılıyor.
Asıl Akıl Tutulması Bizdeydi#
Bankaya ikinci e-postada "IPv6 kullanan müşterilerin her zaman kullanılabilir bir IPv4 adresi yok" diye yazdık. Kulağa mantıklı geliyordu. Büyük ölçüde yanlıştı.
Gerçek: Bugün IPv6 ile gezen kullanıcıların büyük kısmı çift yığınlı (dual-stack), yani hem IPv4 hem IPv6 adresi var. Tarayıcı bir siteye bağlanırken DNS'ten A (IPv4) ve AAAA (IPv6) kayıtlarını alıyor ve RFC 8305 (Happy Eyeballs) ile ikisini yarıştırıyor. Varsayılan tercih IPv6. IPv4 denemesi önerilen 250 milisaniyelik gecikmeyle başlıyor. Hangisi önce bağlanırsa istek onun üzerinden gidiyor, diğeri kapatılıyor.
Teknik Detay: Sunucu bir istekte iki adresi birden görmez. Her bağlantının tek bir adres ailesi var. Cloudflare önündeyse iki ayrı bağlantı oluyor: müşteri ile Cloudflare arası ve Cloudflare ile sizin sunucunuz arası. Müşterinin gerçek adresi size CF-Connecting-IP başlığıyla geliyor ve o başlıkta tek adres var.
Cloudflare, proxy açık her kayıt için kendiliğinden AAAA yayınlıyor. Tarayıcı da IPv6'yı seçiyor. Müşterinin IPv4 adresi büyük ihtimalle vardı, ama tarayıcı ona hiç ihtiyaç duymadığı için biz onu hiç görmedik.
Gerçekten IPv6-only olan mobil ağlarda bile durum değişmiyor. Bu ağlar IPv4 sitelere operatörün NAT64/464XLAT geçidi üzerinden çıkıyor. Sitede AAAA yoksa müşteri size operatörün IPv4 adresiyle geliyor.
Acı Gerçek: Bankaya "IPv4'ü olmayan müşteri" diye bir senaryo anlattık. O müşteriyi biz yaratmıştık.
Çözüm: Kapıya IPv6 Koymamak#
Sanal POS IPv6'yı tanımıyorsa, ödeme akışındaki tarayıcının IPv6 seçmesine fırsat vermemek gerekiyor. Elimizde üç yol vardı:
| Yol | Ne yapar | Bedeli |
|---|---|---|
| Cloudflare'de IPv6 Compatibility kapatmak | Zone için AAAA yayınlanmaz, tarayıcı IPv4 ile gelir | Bütün zone etkilenir; doküman ayarı Enterprise planla sınırlı gösteriyor |
| Ödeme için ayrı bir alt alan adı (gri bulut, yalnızca A kaydı) | Yalnızca ödeme akışı IPv4'e zorlanır | O host'ta Cloudflare koruması yok, origin IP'si görünür |
| Pseudo IPv4 | IPv6 adresinden 240.0.0.0/4 aralığında sahte bir IPv4 üretir | Fraud kontrolü için zorunlu alana gerçek olmayan bir adres gider |
Pseudo IPv4 tüm planlarda var ve ilk bakışta en kolay yol gibi duruyor. Ama Cloudflare'in kendi dokümanı bu aralığın deneysel olduğunu ve üretim trafiğinde kullanılmadığını yazıyor. Bankanın kural koyduğu alana hash'lenmiş bir adres göndermek, sorunu çözmek değil saklamak olur.
Biz birinci yolu seçtik ve ayar 19 Ekim'de 13:49'da (TSİ) devreye girdi. Cloudflare'in kendi çözümleyicisine sorduğumuzda kök alan adı ve www için ikişer A kaydı döndü, AAAA dönmedi. Google'ın genel DNS'i de iki ad için AAAA cevabını boş verdi.
İki küçük not. Birincisi, açık kalan HTTP/2 bağlantıları DNS süresi dolsa da kullanılmaya devam edebilir; sayfası açık kalan bir müşteri bir süre daha IPv6 ile gelebilir. İkincisi, Cloudflare dokümanına göre ayar kapalıyken de Tor (Onion Routing) üzerinden IPv6 trafiği gelebiliyor.
Koda Düşen Pay: Bankaya IPv6 Gitmesin#
DNS ayarı kapıyı kapatıyor ama aralıktan geçen olursa uygulamanın bunu bankaya taşımaması lazım. IPv4 değilse istek hiç gönderilmiyor, loglanıyor, müşteriye anlamsız bir "Declined" yerine sayfayı yenilemesi söyleniyor:
<?php
declare(strict_types=1);
/**
* Bankaya gidecek müşteri IP'sini seçer.
* IPv4 değilse null döner: istek bankaya hiç gönderilmez.
*/
function bankayaGidecekIp(array $server): ?string
{
// Cloudflare arkasında gerçek istemci adresi bu başlıkta.
// Başlığa yalnızca isteğin gerçekten Cloudflare'den geldiğinden
// emin olunan kurulumda güvenin (origin'i Cloudflare IP'lerine kapatın).
$ip = $server['HTTP_CF_CONNECTING_IP'] ?? $server['REMOTE_ADDR'] ?? '';
$ipv4 = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_NO_RES_RANGE);
return $ipv4 === false ? null : $ipv4;
}
Dört örnek adresle PHP 8.3'te çalıştırdığımda çıktı şu oldu:
203.0.113.45 12 karakter -> 203.0.113.45
2001:db8:85a3:8d3:1319:8a2e:370:7348 36 karakter -> GÖNDERME
2001:db8::1 11 karakter -> GÖNDERME
240.16.0.1 10 karakter -> GÖNDERME
Gizli Hazine: Son satır FILTER_FLAG_NO_RES_RANGE sayesinde düşüyor. O bayrak olmadan FILTER_FLAG_IPV4 tek başına 240.16.0.1'i geçerli IPv4 sayıyor. Biri ileride "kolay yol" diye Pseudo IPv4'ü açarsa, sahte adres doğrulamadan sessizce geçip bankaya gidiyor. Bayrak, ayrılmış (reserved) aralıkları reddediyor ve o kapıyı da kapatıyor.
Sorun Hangi Katmanda?#
PHP 8.5'e geçerken statik bir CSS dosyası 500 verdiğinde ders şuydu: sorun PHP'de değil, bir alt katmandaydı. Burada da aynısı oldu. Hata mesajı ödeme modülünü işaret ediyordu, ama karar DNS'te veriliyordu. Hangi adresin geleceğini kodumuz değil, yayınladığımız AAAA kaydı belirliyordu.
Kelimelerin her yerde aynı anlama gelmediği de bir kez daha görüldü. Kentsel Dönüşüm #1'de "salt okuma"nın dört araçta dört ayrı şey olduğunu yazmıştım. Aynı bankanın iki dokümanında aynı müşteri IP'si için hem "biçim şartı yok" hem "IPv4 olmalı" yazıyor.
Bankanın IPv6 desteği eklemesi şart. O gün gelene kadar gişedeki forma uymak zorundayız. Ama bunu müşteriye "eski cüzdanını getir" diyerek değil, kapıyı ona göre açarak yapabiliriz.
Teknolojiyle kalın, AAAA kayıtlarınızı tanıyın.
Yorumlar
0 yorum
Yorum yazın