Python ile IPv6 Adres Yönetimi Script'leri
Selamlar, bu yazımda Python ile küçük bir IPv6 adres yönetim aracı (IPAM) yazacağız. Konuya pek temas etmemiş arkadaşlar için söyleyeyim, IPv6 adres havuzları çok çok büyük; bir /40'ın içinde milyarlarca /56 rahatlıkla sığar. Tabii bu bolluk işin yarısı, asıl mesele kime hangi prefix'i verdiğini takip edebilmek. Lafı uzatmadan başlayalım.
İşin güzel tarafı, Python'un standart kütüphanesindeki ipaddress modülü bu işin ağır yükünün büyük bölümünü zaten kaldırıyor. Biz sadece üzerine bir parça SQLite ve birkaç sade fonksiyon ekleyeceğiz.
IPAM nedir?
IPAM (IP Address Management) kısaca IP adres havuzlarını ve bunların kime, ne zaman tahsis edildiğini izleyen sistemlerin genel adı. Kurumsal tarafta NetBox, phpIPAM gibi araçlar var ama küçük bir ISP, ev lab'ı ya da tek bir ekibin altyapısı için 200 satırlık bir Python script çoğu zaman yeter. Bence buradaki kritik nokta şu: IPv6'da artık 'adres tasarrufu' diye bir derdimiz yok, asıl izlemek istediğimiz şey hangi prefix'in kime gittiği ve havuzun ne kadarının dolduğu.
ipaddress modülü temelleri
Modülün iki kahramanı var: ip_network ve subnets. İlki bir prefix'i nesneye çeviriyor, ikincisi de o prefix'in içindeki alt ağları üretiyor. Şuna bir bakalım:
import ipaddress
pool = ipaddress.ip_network('2001:db8:1000::/40')
print(f'Toplam adres: {pool.num_addresses:,}')
ilk_uc = list(pool.subnets(new_prefix=56))[:3]
for net in ilk_uc:
print(net)
subnets(new_prefix=56) bize /40'ı /56'lara böler. Bu bir generator, yani milyarlarca alt ağı bellekte tutmuyor; biz for ile gezerken üretiyor. Performans açısından önemli bir detay, sonradan değineceğim.
SQLite tabanlı küçük IPAM
Şimdi havuzları ve tahsisleri saklayan bir sınıf yazalım. SQLite seçtim çünkü tek dosya, sıfır kurulum, küçük ekip için fazlasıyla yeterli.
import sqlite3
import ipaddress
from datetime import datetime, timezone
class IPv6IPAM:
def __init__(self, db_path: str = 'ipam.db'):
self.conn = sqlite3.connect(db_path)
self.conn.execute('PRAGMA foreign_keys = ON')
self.conn.executescript('''
CREATE TABLE IF NOT EXISTS pools (
id INTEGER PRIMARY KEY,
name TEXT UNIQUE NOT NULL,
prefix TEXT NOT NULL,
prefixlen INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS allocations (
id INTEGER PRIMARY KEY,
pool_id INTEGER REFERENCES pools(id),
prefix TEXT NOT NULL UNIQUE,
prefixlen INTEGER NOT NULL,
assignee TEXT NOT NULL,
assigned_at TEXT NOT NULL,
released_at TEXT
);
''')
self.conn.commit()
def add_pool(self, name: str, prefix: str):
net = ipaddress.ip_network(prefix, strict=True)
if net.version != 6:
raise ValueError('Havuz IPv6 prefix olmalı')
self.conn.execute(
'INSERT OR IGNORE INTO pools (name, prefix, prefixlen) VALUES (?,?,?)',
(name, str(net.network_address), net.prefixlen),
)
self.conn.commit()
Burada strict=True önemli. Mesela 2001:db8::1/40 gibi host bitleri sıfır olmayan bir değer gelirse hata fırlatıyor. Yoksa havuzun başlangıç adresi yanlış kaydedilir, sonra bütün hesap kayar.
Bir sonraki uygun prefix'i bulmak
İşin asıl mantığı tahsis fonksiyonunda. Havuzun içindeki alt ağları sırayla geziyoruz, ilk çakışmayan adayı veriyoruz:
def allocate(self, pool_name: str, alloc_len: int, assignee: str) -> str | None:
row = self.conn.execute(
'SELECT id, prefix, prefixlen FROM pools WHERE name = ?',
(pool_name,),
).fetchone()
if not row:
raise ValueError(f'{pool_name} bulunamadı')
pool_id, pool_prefix, pool_len = row
pool_net = ipaddress.ip_network(f'{pool_prefix}/{pool_len}')
aktif = [
ipaddress.ip_network(f'{p}/{ln}')
for p, ln in self.conn.execute(
'SELECT prefix, prefixlen FROM allocations '
'WHERE pool_id = ? AND released_at IS NULL',
(pool_id,),
)
]
for aday in pool_net.subnets(new_prefix=alloc_len):
if not any(aday.overlaps(x) for x in aktif):
self.conn.execute(
'INSERT INTO allocations '
'(pool_id, prefix, prefixlen, assignee, assigned_at) '
'VALUES (?,?,?,?,?)',
(pool_id, str(aday.network_address), alloc_len,
assignee, datetime.now(timezone.utc).isoformat()),
)
self.conn.commit()
return f'{aday.network_address}/{alloc_len}'
return None
overlaps metodu IPv4/IPv6 fark etmeden iki ağın kesişip kesişmediğini söylüyor. Tek başına bu metot bile bir IPAM aracının kalbidir diyebilirim.
Kullanım raporu
Müşteri sayısı 100'e dayandığında ister istemez 'havuz ne kadar doldu?' sorusu geliyor. Hızlıca:
def kullanim_raporu(conn, hedef_uzunluk: int = 56):
for pool_id, name, prefix, plen in conn.execute(
'SELECT id, name, prefix, prefixlen FROM pools'
):
toplam = 2 ** (hedef_uzunluk - plen) if plen <= hedef_uzunluk else 0
aktif = conn.execute(
'SELECT COUNT(*) FROM allocations '
'WHERE pool_id = ? AND prefixlen = ? AND released_at IS NULL',
(pool_id, hedef_uzunluk),
).fetchone()[0]
oran = (aktif / toplam * 100) if toplam else 0
print(f'{name}: {aktif}/{toplam} ({oran:.1f}%)')
/40 havuzdan /56 dağıtıyorsanız teorik kapasite 2 ** 16, yani 65 bin müşteri. Kâğıt üstünde rahat görünür ama %80'i geçince yeni havuz açmak için harekete geçmenizi öneririm.
Sık karşılaşılan hatalar
- strict=False ile havuz tanımlamak: Host bitleri sıfır olmayan bir değeri sessizce kabul ettirirsiniz, sonradan hesap tutmaz. Havuzlar için her zaman
strict=True. - Çok büyük havuzu tek seferde listeye almak:
list(pool.subnets(...))yazmayın, generator olarak gezin./40'tan/64üretmeye kalkarsanız bellekte 16 milyon nesne oluşur. - Sürüm karışıklığı:
ip_networkIPv4 prefix'i de kabul eder.net.version != 6kontrolü olmadan IPAM'inizin içine bir gün bir/24IPv4 düşer ve raporlar bozulur. - released_at filtresini unutmak: Serbest bırakılmış prefix'leri 'aktif' saymak yıllar önce kapanmış müşterilerin slotlarını ölü tutar; gerçek doluluk oranını göremezsiniz.
Kapanış
Standart kütüphanedeki ipaddress modülü ve birkaç tablo SQLite ile aslında üretime yakın bir IPv6 IPAM çıkmış oldu. Bana sorarsanız bu boyuttaki bir araç küçük ISP'ler ve ev lab'ları için fazlasıyla iş görür; ölçek büyüdüğünde SQLite'ı PostgreSQL'e çevirmek ve üzerine bir FastAPI katmanı koymak yeterli. Umarım faydalı bir içerik olmuştur, görüşmek üzere.
