Python ile IPv6 Soket Programlama

Selamlar, bu yazımda Python'un socket modülü ile IPv6 üzerinden nasıl sunucu ve istemci yazacağımıza bakacağız. Konu IPv4'ten gelen arkadaşlar için ilk başta biraz tuhaf duruyor, çünkü adres tuple'ı bir anda iki elemandan dört elemana çıkıyor. Hadi adım adım gidelim.

IPv6 artık 'gelecekte düşünürüz' kategorisinden çıkalı çok oldu. Mobil operatörler büyük ölçüde IPv6'ya geçti, bulut sağlayıcıları çift yığın (dual-stack) sunuyor, bazı iç ağlar zaten sadece IPv6 konuşuyor. Yazdığınız kod IPv4'e gömülü kalırsa, bir gün biri 'production'da AAAA kaydı çözülmüyor' diye geldiğinde tatsız sürpriz olabiliyor.

AF_INET6 ve dort elemanli adres

Python'da IPv6 soketi açmak aslında çok basit, socket.AF_INET6 kullanıyoruz. Asıl fark adres tuple'ında: IPv4'te (host, port) yeterken, IPv6'da (host, port, flowinfo, scope_id) şeklinde dört elemanlı bir yapı var. flowinfo çoğu zaman 0, scope_id ise sadece link-local adresler için anlam kazanıyor.

import socket

sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
sock.bind(('::', 8080, 0, 0))
sock.listen(5)
print('IPv6 server [::]:8080 dinliyor')

Burada '::' IPv6'nın 'her arayüz' adresi, yani IPv4'teki '0.0.0.0''ın muadili. Bind ederken bile dört elemanlı tuple beklendiğine dikkat edin; bu, IPv4 alışkanlığıyla en sık unutulan detay.

Basit bir TCP istemci

Sunucu tarafı çalışınca istemci yazmak da çok ayrıksı değil. Bağlanırken yine dört elemanlı tuple veriyoruz:

import socket

def ipv6_connect(host: str, port: int) -> socket.socket:
    sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
    sock.settimeout(10)
    sock.connect((host, port, 0, 0))
    return sock

client = ipv6_connect('::1', 8080)
data = client.recv(1024)
print(f'Sunucudan gelen: {data.decode()}')
client.close()

::1 IPv6'nın loopback adresi, yani 127.0.0.1'in karşılığı. Yerelde test ederken işinizi görür.

getaddrinfo ile protokol bagimsiz kod

Şahsi kanaatim, gerçek dünyada üretim kodu yazıyorsanız doğrudan AF_INET6 ya da AF_INET seçmek yerine socket.getaddrinfo ile sistemin önerdiği aileyi kullanmak çok daha sağlıklı. Bu sayede aynı kod hem IPv4-only hem IPv6-only hem de çift yığınlı ortamlarda çalışıyor.

import socket

def connect_anywhere(host: str, port: int) -> socket.socket:
    infos = socket.getaddrinfo(
        host, port,
        family=socket.AF_UNSPEC,
        type=socket.SOCK_STREAM,
        flags=socket.AI_ADDRCONFIG,
    )

    last_err = None
    for family, socktype, proto, _canon, sockaddr in infos:
        sock = None
        try:
            sock = socket.socket(family, socktype, proto)
            sock.settimeout(10)
            sock.connect(sockaddr)
            return sock
        except OSError as err:
            if sock is not None:
                sock.close()
            last_err = err

    raise ConnectionError(f'{host}:{port} icin baglanti kurulamadi') from last_err

AF_UNSPEC ile 'IPv4 da IPv6 da olur' diyoruz; AI_ADDRCONFIG ise sistemde gerçekten yapılandırılmış aileleri döndürüyor, böylece IPv6'sı olmayan makinede boşa AAAA denemiyoruz. Liste sırası işletim sisteminin tercihini yansıtır; biz de baştan deniyor, ilk başaranı dönüyoruz.

Cift yigin: tek soketle hem IPv4 hem IPv6

Çoğu Linux dağıtımında bir IPv6 soketi varsayılan olarak IPv4 trafiğini de kabul edebilir; tek yapmamız gereken IPV6_V6ONLY bayrağını kapatmak. Böylece IPv4 istemciler IPv4-mapped IPv6 adresi (::ffff:1.2.3.4) ile sizin sunucuya rahatça düşer.

import socket

server = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

if socket.has_dualstack_ipv6():
    server.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)

server.bind(('::', 8080, 0, 0))
server.listen(5)

Bence bu, küçük servisler için en pratik kurulum. Büyük yüklerde IPv4 ve IPv6'yı ayrı soketlere ayırmak isteyebilirsiniz, ama o ayrı bir tartışma.

Scope ID ne zaman lazim?

Link-local adresler (fe80::/10) tek başına anlamlı değil, çünkü aynı adres birden fazla arayüzde olabilir. Bu yüzden hangi arayüzü kastettiğimizi scope_id ile söylüyoruz:

import socket

scope_id = socket.if_nametoindex('eth0')
sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
sock.connect(('fe80::1', 8080, 0, scope_id))

Global IPv6 adreslerinde scope_id'yi 0 bırakın, yetişir.

Sik karsilasilan hatalar

  • Adres tuple'ini iki elemanli birakmak: bind(('::', 8080)) yazarsanız Python TypeError atar. Dört elemanlı tuple zorunlu, alıştırmak biraz vakit alıyor.
  • '::' yerine '::1' bind etmek: ::1 sadece localhost'tan gelen istemcileri kabul eder. Container ya da uzak istemci varsa bağlantı reddedilir; sunucuyu '::' ile bağlayın.
  • IPV6_V6ONLY varsayilanini sorgulamamak: Linux'ta genellikle 1, BSD'de 0 olabiliyor. Çift yığın istiyorsanız bayrağı açıkça 0 yapın, davranışı işletim sistemine bırakmayın.
  • Link-local adreste scope_id unutmak: fe80::1'e scope'suz bağlanmaya çalışırsanız Invalid argument alırsınız. if_nametoindex ile arayüz indeksini eklemek gerek.

Kapanis

Bu yazımızda Python'un socket modülü ile IPv6 sunucu ve istemcinin nasıl yazıldığına, dört elemanlı adres tuple'ına, çift yığın kuruluma ve scope_id'nin ne işe yaradığına baktık. Bana sorarsanız, yeni yazdığınız kodda doğrudan AF_INET6 seçmek yerine getaddrinfo ile gitmek hem daha az sürpriz çıkarır hem de ileride IPv6-only ortamlara taşınmayı kolaylaştırır. Umarım faydalı olur, görüşmek üzere.