Semptom
Bir kurumun Sophos Firewall'unu (XGS serisi, SFOS 22.0) kendi log alıcıma bağlıyordum. Sophos tarafındaki syslog sunucusu tanımı şuydu: biçim "Standard syslog protocol", "Secure log transmission" açık, TCP 6514 üzerinden TLS. Alıcı, Python ile yazdığım küçük bir servisti: TLS'i karşılıyor, akışı iletilere bölüyor, ayrıştırıp veritabanına yazıyordu. Duvar dakikada yaklaşık 950–1.300 satır üretiyordu.
İlk bağlantıda TLS el sıkışması sorunsuz tamamlandı, bağlantı açık kaldı, baytlar akmaya başladı. İşlenen satır sayısı: sıfır. Tampon 64 KB'a ulaşınca alıcının "çok büyük çerçeve" sayacı bir arttı, tampon atıldı ve döngü baştan başladı. Hata yoktu, uyarı yoktu; yalnız sıfır.
İkinci semptom aynı gün geldi (27 Eyl 2026). Alıcıyı 15:23:24'te yeniden başlattım. Bağlantı geri gelmedi. 22 dakika boyunca alıcının logunda tek satır yoktu.
Yanlış yollar
- "TLS üzerinden syslog'un çerçevesi bellidir." TLS taşımasını tanımlayan RFC 5425 oktet sayımı öngörür: her ileti "uzunluk, boşluk, ileti" biçiminde gelir. Pratikte birçok gönderici ise iletiyi LF ile bitirir. Alıcımın ilk sürümü sonlandırıcı olarak yalnız LF tanıyordu ve bunun yeteceğini varsaymıştım. Sophos ne uzunluk öneki koyuyordu ne LF.
- "Gönderici yeniden dener." Çoğu istemci bağlantı kopunca aralıklarla yeniden bağlanmayı dener. Bunu varsayıp bekledim. 22 dakika bekledim.
- Değişiklik yapmadan Save. Sophos'ta syslog sunucusu tanımını açıp hiçbir şeyi değiştirmeden kaydettim; bağlantıyı yeniden kurdurur diye düşündüm. Kurdurmadı.
- "Alıcıyı yeniden başlatmak zararsızdır." Kendi sunucumdaki bir servisi yeniden başlatmanın karşı tarafa bir maliyeti olmayacağını varsaydım. Bu vakadaki en pahalı varsayım buydu (aşağıda).
Doğru test
Çerçeve için tahmin yürütmeyi bırakıp göndericinin ilk baytlarına baktım. Alıcıya, bağlantının ilk parçasının başını ve içindeki LF, CR ve NUL sayısını log'a yazan bir teşhis satırı ekledim.
first chunk … LF=0 CR=0 NUL=1 octet_prefix=False head=b'<134>'
LF ve CR yok: satır sonu yok. octet_prefix=False: uzunluk öneki yok, ileti
doğrudan öncelik değeriyle başlıyor (<134> = local0, info). Tek ayırıcı
NUL. (… = kısaltıldı)
Bu tek satır üç hipotezi birden eledi: TLS çalışıyor (okunabilir metin geliyor), oktet sayımı
yok, LF yok. Geriye tek aday kaldı: her ileti \x00 ile bitiyor.
Yeniden bağlanma için alıcının logu boşken üç olasılık vardı: Sophos deniyor
ama sunucudaki paket filtresi düşürüyor; Sophos deniyor ama TLS el sıkışması alıcıda
reddediliyor; Sophos hiç denemiyor. tcpdump paketleri arayüzde, yerel paket
filtresinden önce yakalar. SYN görülüyorsa ilk ikisinden biri, görülmüyorsa üçüncüsü.
tcpdump -ni eth0 'tcp port 6514 and (tcp[tcpflags] & (tcp-syn|tcp-rst) != 0)'
# 15:23:24 alıcı yeniden başlatıldı
# ≈ +1 sn Sophos → SYN sunucu → RST (dinleyici henüz açılmamıştı)
# 22 dk tek SYN yok
Yeniden başlatma anındaki tek SYN, yolun açık olduğunu da gösterdi: paket geliyor, aradaki duvarlar geçiriyor. Sonraki sessizlik tamamen gönderici tarafında.
Kök neden
1 · Çerçeve: NUL
SFOS 22.0, "Standard syslog protocol" ve TLS ile gönderirken her iletiyi tek bir
\x00 baytıyla bitiriyor: LF yok, oktet sayımı yok. Alıcım LF arıyordu. Bulamadığı
için akışın tamamını bitmemiş tek bir ileti sandı ve 64 KB sınırında "çok büyük" deyip attı.
Satır sayısı bu yüzden yaklaşık değil, tam sıfırdı.
Standart bu kapıyı açık bırakıyor: RFC 6587, sonlandırıcılı çerçevede LF'nin yanında uygulamaya özgü sonlandırıcılara da yer verir. "Standarda uygun" bir alıcı bile göndericiyi görmeden çerçeveyi bilemez.
Çerçeveleme, protokolün en sessiz katmanıdır: yanlışsa hata değil, hiçbir şey üretir. Göndericinin ilk baytlarını görmeden çerçeve varsayma. LF, CR, NUL ve uzunluk önekini sayan tek bir teşhis satırı, sorunu tek bakışta gösterir.
2 · Yeniden bağlanma: tek deneme
Gözlediğim davranış şu: bağlantı koptuğunda SFOS 22.0 yaklaşık bir saniye içinde bir kez yeniden dener. O anda dinleyen bir soket yoksa çekirdek RST döner ve Sophos bir daha denemez. Bunun bir hata mı tasarım mı olduğunu bilmiyorum; ölçtüğüm bu.
Bağlantıyı geri getiren tek şey, Log settings tablosunda kendi syslog sunucumun sütunundaki bir kutuyu kapatıp Apply, yeniden açıp Apply yapmak oldu. Değişiklik olmadan Save'in işe yaramaması bununla uyumlu; tahminim, değişmeyen ayarın syslog bileşenini yeniden başlatmadığı — ama bu bir tahmin. İlk Apply'dan sonra Sophos iki kısa bağlantı açıp kapatıyor (0 satır, ardından eof); bu normal, arkasından kalıcı bağlantı geliyor.
3 · Bağlam: erişim pahalıydı
Kurum duvarının yönetimine yalnız VPN üzerinden erişilebiliyordu; kutu kapat/aç adımı her seferinde bir yönetim oturumu demekti. Yani alıcıyı her yeniden başlatışım — bir dağıtım, sunucunun yeniden açılması, bir çökme — log akışını bir sonraki yönetim oturumuna kadar durduruyordu. Altı saatlik sessizlik alarmı durumu yakalıyordu, ama yakalamak düzeltmek değil.
Çözüm
1 · Çerçeveleyici LF'yi de NUL'u da tanıyor
Çerçeveleyici artık üç biçimi kabul ediyor: RFC 6587 oktet sayımı, LF (önündeki CR kırpılarak) ve NUL. İletiler arasındaki boş ayırıcılar atlanıyor; sonlandırıcısı gelmeden sınırı aşan çerçeve atılıp sayılıyor.
import re
OKTET = re.compile(rb"([1-9][0-9]{0,6}) ")
def _son(tampon):
"""İlk sonlandırıcının yeri: LF ya da NUL, hangisi önce gelirse; yoksa -1."""
i, j = tampon.find(b"\n"), tampon.find(b"\x00")
return j if i < 0 else (i if j < 0 else min(i, j))
class Cerceveleyici:
def __init__(self, azami=65536):
self.azami = azami
self.tampon = bytearray()
self.cok_buyuk = 0
def besle(self, veri):
self.tampon += veri
iletiler = []
while True:
self.tampon = self.tampon.lstrip(b"\r\n\x00 ") # iletiler arası ayırıcılar
if not self.tampon:
break
m = OKTET.match(self.tampon) # RFC 6587 oktet sayımı
if m:
n, bas = int(m.group(1)), m.end()
if n > self.azami:
raise ValueError("oktet çerçevesi sınırı aşıyor")
if len(self.tampon) - bas < n:
break # ileti yarım, devamını bekle
iletiler.append(bytes(self.tampon[bas:bas + n]))
del self.tampon[:bas + n]
continue
i = _son(self.tampon) # LF ya da NUL
if i < 0:
if len(self.tampon) > self.azami: # eski hatanın izi: 64 KB, 0 satır
self.cok_buyuk += 1
self.tampon.clear()
break
iletiler.append(bytes(self.tampon[:i]).rstrip(b"\r"))
del self.tampon[:i + 1]
return iletiler
Gerçek sürümde yarım gelen uzunluk öneki ve aşırı büyük çerçevenin kalanını bir sonraki sonlandırıcıya kadar atlama gibi uç durumlar da var; burada çıkarıldı.
2 · Kısa vade: kutuyu kapat, aç
- Sophos'ta Log settings tablosunu aç.
- Kendi syslog sunucunun sütununda bir kutunun işaretini kaldır, Apply.
- Aynı kutuyu yeniden işaretle, Apply.
- Alıcıda önce iki kısa bağlantı (0 satır, eof), ardından kalıcı bağlantı görülmeli.
3 · Kalıcı: dinleyiciyi systemd tutuyor (28 Eyl 2026)
Sorunun özü zamanlamaydı: alıcı yeniden başlarken dinleyen soket bir an yok oluyor ve Sophos'un tek denemesi tam o ana düşüyordu. Dinleyen soketi servisin elinden alıp systemd'ye verirsem port hiç kapanmaz. Servis yeniden başlarken gelen bağlantıyı çekirdek kabul kuyruğuna alır; yeni süreç açılınca kuyruktan devralır. Bunun adı socket activation.
# /etc/systemd/system/syslog-alici.socket
[Unit]
Description=Syslog TLS dinleyicisi (6514)
[Socket]
ListenStream=6514
Accept=no
Backlog=64
IPAddressDeny=any
IPAddressAllow=localhost 192.0.2.10
[Install]
WantedBy=sockets.target
# /etc/systemd/system/syslog-alici.service
[Unit]
Description=Syslog TLS alıcısı
Requires=syslog-alici.socket
After=network-online.target syslog-alici.socket
[Service]
Type=simple
ExecStart=/usr/bin/python3 /usr/local/lib/syslog-alici/alici.py
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
# Geçiş: önce servis kendi dinleyicisini bıraksın
systemctl stop syslog-alici.service
systemctl enable --now syslog-alici.socket
systemctl start syslog-alici.service
Birimler aynı adı taşıdığı için systemd soketi servise kendiliğinden eşler.
192.0.2.10, Sophos'un adresi yerine konmuş bir örnektir.
TLS'in bu düzende yeri değişmiyor. systemd yalnız düz TCP dinleyicisini tutuyor
(ListenStream, Accept=no). Servis bu soketi sd_listen_fds
protokolüyle devralıyor: systemd ortam değişkenlerine LISTEN_PID ve
LISTEN_FDS yazıyor, soket 3 numaralı dosya tanımlayıcısı olarak geliyor. Bağlantıyı
servis kabul ediyor ve TLS el sıkışmasını kabul ettiği bağlantı üzerinde kendisi yapıyor;
sertifika ve TLS ayarları serviste kalıyor.
import os, socket, ssl
def dinleyici(port=6514):
"""systemd'den devral; yoksa (elle çalıştırma, test) kendin aç."""
if (os.environ.get("LISTEN_PID") == str(os.getpid())
and int(os.environ.get("LISTEN_FDS", "0")) >= 1):
for k in ("LISTEN_PID", "LISTEN_FDS", "LISTEN_FDNAMES"):
os.environ.pop(k, None) # alt süreçlere sızmasın
return socket.socket(fileno=3) # SD_LISTEN_FDS_START = 3
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(("0.0.0.0", port))
s.listen(64)
return s
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ctx.minimum_version = ssl.TLSVersion.TLSv1_2
ctx.load_cert_chain("/etc/syslog-alici/tls.crt", "/etc/syslog-alici/tls.key")
lsock = dinleyici()
while True:
sock, adres = lsock.accept()
conn = ctx.wrap_socket(sock, server_side=True) # TLS el sıkışması serviste
... # gerçekte her bağlantı ayrı iş parçacığında: oku → Cerceveleyici.besle() → ayrıştır
Bir ayrıntı: systemd'nin IPAddressAllow/IPAddressDeny ayarları, socket
activation ile devralınan soketlere servis biriminden uygulanmaz; dinleyen soket artık
.socket birimine aittir. Kaynak adres süzgecini bu yüzden soket birimine yazdım;
servis içindeki izin listesi ve sunucunun paket filtresi de yerinde duruyor.
Geçişin kendisi de bir kopma: servis kendi dinleyicisini bırakmadan soket birimi aynı portu alamaz. Bu yüzden geçişin kurumun bir sonraki yönetim oturumuyla eşleşmesi ve sonunda bir kez daha kutu kapat/aç yapılması gerekiyordu.
Doğrulama: soket birimi devredeyken alıcı servisini yeniden başlattım; log akışı kutu kapat/aç gerekmeden sürdü. Mevcut TLS bağlantısı servisle birlikte kapanıyor, bu değişmedi. Değişen, Sophos'un tek yeniden denemesinin artık RST'ye değil, bekleyen bir kuyruğa düşmesi.
Kalıcı ders
- Çerçeveyi varsayma, say. Standart bir şey söyler, gelenek başka bir şey, gönderici üçüncü bir şey yapabilir. İlk baytlardaki LF, CR, NUL ve uzunluk önekini sayan bir teşhis satırı her yeni gönderici için ilk adım olmalı.
-
Göndericinin yeniden bağlanma davranışı kabul testinin parçasıdır. Kurulumu
bitirmeden önce alıcıyı bir kez bilerek yeniden başlat ve
tcpdumpile SYN'leri izle. Bu vakada gönderici bir kez denedi ve vazgeçti; bunu canlıda değil, kurulumda öğrenmek isterdim. - Yeniden başlatılan süreç ile kesintisiz kalması gereken port aynı elde olmamalı. Socket activation tam bunun için var: dinleyici systemd'de, iş mantığı serviste.
- Karşı tarafa erişim pahalıysa, kendi tarafındaki her yeniden başlatma bir operasyon maliyetidir. Sessizlik alarmı arızayı bildirir ama düzeltmez; düzeltme mimaride.