teknikbakis

64 KB geldi, sıfır satır işlendi; alıcıyı yeniden başlatınca Sophos 22 dakika tek SYN göndermedi

Sophos Firewall TLS üzerinden syslog gönderirken her iletiyi bir NUL baytıyla bitiriyor ve bağlantı koptuğunda bir kez deneyip vazgeçiyor. Birincisini çerçeveleyicide, ikincisini dinleyiciyi systemd'ye devrederek çözdüm.

DOSYA TB-2026-011
SİSTEM Sophos SFOS 22.0 · Python · systemd
KATMAN Log taşıma / TLS çerçeveleme
SÜRE 2 gün · 27–28 Eyl 2026
DURUM ÇÖZÜLDÜ
KANIT 4 ek

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.

EK-1 İlk parçanın teşhis satırı
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ü.

EK-2 Ayırt edici test: SYN geliyor mu?
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.

ÇIKARIM

Ç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.

EK-3 Çerçeveleyici (Python, sadeleştirildi)
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ç

  1. Sophos'ta Log settings tablosunu aç.
  2. Kendi syslog sunucunun sütununda bir kutunun işaretini kaldır, Apply.
  3. Aynı kutuyu yeniden işaretle, Apply.
  4. 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.

EK-4 Soket ve servis birimleri (jenerik adlarla)
# /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 tcpdump ile 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.