Ahmet Bilal Yazıcıoğlu
Ahmet Bilal Yazıcıoğlu
← Back to blog
August 25, 2026/5 min read/0 viewstr

tincan: Terminalde Sunucusuz Sesli Sohbet

rustp2pterminal

Arkadaşlarla oyun oynarken sesli sohbet için hep birinin sunucusuna bağlanıyoruz. Discord kapanırsa, hesabın askıya alınırsa, ya da sadece "şu konuşma kimsenin sunucusundan geçmesin" dersen elinde pek bir şey kalmıyor. Mumble tarzı çözümler var ama onda da birinin VPS kiralayıp port açması gerekiyor. Ben de "acaba hiç sunucu olmadan olur mu" diye bakmaya başladım ve tincan çıktı ortaya.

Mantık şu: uygulamayı ilk açan kişi odayı kuruyor, ekrana bir davet kodu basılıyor, o kodu arkadaşlarına gönderiyorsun, onlar dünyanın herhangi bir yerinden bağlanıyor. VPN yok, port yönlendirme yok, hesap açma yok. Hepsi terminalde.

tincan host --name ahmet --room istanbul --password gizli
tincan join n73w-kuqc-uog2-... --name mehmet --password gizli

Davet kodu aslında bir public key

En sevdiğim kısım burası. Kod rastgele üretilmiş bir oda numarası değil, koordinatörün public key'inin ta kendisi. Bağlantı için iroh kullanıyorum: karşı tarafı public key'iyle buluyorsun, QUIC bağlantıyı uçtan uca şifreliyor ve karşındakinin gerçekten o kişi olduğunu aynı anahtarla doğruluyor. Yani "sunucu yok" derken ortada gerçekten bir sunucu yok, sadece iki uç var.

NAT delme çoğu durumda başarılı oluyor, olmazsa trafik bir relay üzerinden akıyor. Relay içeriği çözemiyor, sadece iletiyor. Kodun 52 karakter olmasının sebebi de bu: public key'in kendisi olduğu için kısaltılamıyor. Kopyala-yapıştır için sorun değil ama telefonda birine okutmayı deneme, ben denedim.

Ses koordinatörden geçmiyor

Burada iki ayrı düzlem var ve bunları ayırmak projenin en doğru kararıydı.

Kontrol düzlemi bir yıldız. Üye listesi, kanallar, yazışma — hepsi koordinatörün üzerinden akıyor. Trafiği çok küçük, saniyede birkaç yüz bayt.

Ses düzlemi ise mesh. Aynı kanaldaki herkes birbirine doğrudan bağlanıyor ve Opus paketlerini QUIC datagram olarak yolluyor. Eğer sesi de koordinatörden geçirseydim odayı açan kişinin upload'ı herkesin ses kalitesini belirlerdi; ev internetiyle bu iş yürümez. Şu haliyle 6 kişilik bir odada kişi başı ~160 kbps upload yetiyor.

Bunun bedeli tabii ki ölçek: mesh'te herkes herkese gönderdiği için 8+ kişide işin içine koordinatörün sesi miksleyip tek akış olarak dağıtması (SFU) girmesi gerekiyor. Şimdilik 2-6 kişi hedefliyorum, arkadaş grubu için yeterli.

Önce ölçtüm, sonra yazdım

Projeye başlarken tek bir korkum vardı: "ya iki farklı NAT arkasındaki makine hiç bağlanamazsa?" O yüzden uygulamanın tek satırını yazmadan önce iki tane atılabilir probe yazdım. examples/ping.rs iki makine arasında ses temposunda (20 ms'de bir paket) datagram yollayıp bağlantının doğrudan mı relay üzerinden mi kurulduğunu, RTT dağılımını ve paket kaybını ölçüyor. examples/loopback.rs ise mikrofon → Opus → çözücü → hoparlör zincirinin gecikmesini ölçüyor.

İkisi de üründe kullanılmıyor, sadece "bu tasarım tutar mı" sorusunu cevaplamak için yazıldı. Geriye dönüp bakınca en karlı iki gün buydu. Çalışmasaydı bütün mimariyi çöpe atacaktım ve bunu 3 hafta sonra öğrenmek çok daha acı olurdu.

Ses zinciri ve gerçek dünyanın pislikleri

Kağıt üzerinde ses basit: mikrofondan al, kodla, yolla, çöz, çal. Pratikte araya bir sürü şey giriyor.

48 kHz ve 20 ms'lik (960 örnek) çerçevelerle çalışıyorum. Paketler farklı gecikmelerle geldiği için her peer'ın kendi jitter tamponu var, hedefi 3 çerçeve — yani ~60 ms. Bu sayı bilerek küçük: büyütürsen kopma azalıyor ama konuşma "telsiz" gibi olmaya başlıyor. Gelen sesler mikslenirken bir limitleyici devrede, yoksa üç kişi aynı anda konuşunca sinyal kırpılıyor. Konuşma algılama (VAD) hem kimin konuştuğunu göstermek hem de sessizken paket yollamamak için var.

Bir de şu var: yeniden örnekleme yapmıyorum. Cihazın 48 kHz'de çalışmıyorsa tincan bunu açıkça söyleyip yazışma moduna düşüyor. Sessizce bozuk ses üretmesindense "ben bunu yapamam" demesi daha iyi geldi.

Sustur tuşu mesaj gönderiyordu

Favori hatam. Ses kısayollarını başta Ctrl+M (sustur) ve Ctrl+J yapmıştım, mantıklı geliyordu. Sonra test ederken mikrofonu kapatmaya çalıştıkça kanala boş mesajlar düşmeye başladı.

Sebebi şu: terminalde Ctrl+M 0x0D, Ctrl+J 0x0A demek — yani ikisi de Enter'ın ta kendisi. Terminal sana "Ctrl+M'e bastı" demiyor, "Enter'a bastı" diyor, ayırt etmek mümkün değil. O yüzden şu an ses kısayolları F2/F3/F5. Terminal uygulaması yazarken tuşların aslında ne olduğunu bilmek gerekiyormuş, öğrendim.

Aynı sebepten bas-konuş da tam olarak "basılı tutma" değil: terminaller tuş bırakma olayını genelde bildirmiyor, o yüzden --ptt modunda F4 bir aç/kapat düğmesi gibi çalışıyor. Dürüst olmak gerekirse bu bir eksiklik, README'de de öyle yazıyor.

Parola tel üzerinden hiç geçmiyor

Odaya parola koyabiliyorsun ama parolanın kendisi ağa hiç çıkmıyor. Koordinatör rastgele bir nonce yolluyor, istemci Argon2id(parola, nonce) sonucunu geri gönderiyor. Nonce her bağlantıda yenilendiği için yakalanan bir kanıt tekrar kullanılamıyor.

Buradaki ince nokta şu: parola şifreleme için değil, katılım denetimi için. Şifrelemeyi QUIC zaten yapıyor. Bu ikisini karıştırmamak lazım, yoksa "parola koydum demek ki güvenli" gibi yanlış bir his oluşuyor.

Ufak bir geliştirme notu: Argon2 debug derlemede kabul edilemez derecede yavaştı, bağlanmak saniyeler sürüyordu. Çözüm Cargo'da sadece o paketi optimize etmek oldu:

[profile.dev.package.argon2]
opt-level = 3

Testler internete çıkmıyor

93 test var ve bunların içinde kontrol düzlemi ile ses mesh'inin entegrasyon testleri de dahil. İlginç olan kısmı: bu testler gerçek iki iroh endpoint'i ve gerçek QUIC bağlantısı kuruyor ama relay ve keşif kapalı, adresler elle tanıtılıyor. Yani internet olmadan çalışıyorlar. Ses testleri de ses donanımı kullanmıyor, doğrudan mesh'in uçlarına bağlanıyorlar.

CI'da veya uçakta çalışan bir test paketi, "makinemde geçiyordu" demekten çok daha rahat.

Neler eksik

Dürüst olmak gerekirse birkaç şey var. Koordinatör tek hata noktası: host çıkarsa oda dağılıyor, lider devrini bilerek MVP dışında bıraktım. Bağlantının ilk saniyesi relay üzerinden akıyor, sonra doğrudan bağlantıya geçiyor, o yüzden odaya girer girmez ufak bir gecikme hissedebiliyorsun. Ve yukarıda dediğim gibi 8+ kişi bu mimariyle olmuyor.

Yine de asıl istediğim şey oldu: terminali açıp bir komut yazıyorsun, arkadaşına 52 karakter gönderiyorsun ve kimsenin sunucusuna ihtiyaç duymadan konuşuyorsunuz.

Kod burada: github.com/bilalyazicioglu/tincan-cli. Rust 1.91+, cmake ve pkg-config gerekiyor (Opus kaynaktan derleniyor), gerisi cargo build --release.

Have thoughts on this? Send me a note.