JIT WireGuard teknolojisi
(fly.io)- Fly.io,
flyctlile Fly Machines arasındaki doğrudan iletişimi korurken WireGuard gateway üzerindeki durum yükünü azaltmak için, peer'leri önceden kurmak yerine bağlantı anında çekirdeğe ekleyen bir yönteme geçti - Eski akışta GraphQL API, peer yapılandırmasını NATS RPC ile iletiyor,
wggwdbunu SQLite'a ve Linux çekirdeğindeki WireGuard katmanına kaydediyor, ardındanflyctlbağlanıyordu - NATS mesaj kaybı ile CI işlerinin tek seferlik peer üretimi birleşince, gateway üzerinde yüz binlerce yeniden kullanılmayan peer birikti; bu da çekirdek işlemlerini ve yeniden başlatma sırasında yüklemeyi yavaşlattı
- Yeni yöntem,
handshake initiationpaketini BPF filtresi veya WebSockets alım yolunda yakalıyor, Noise handshake'in bir kısmını çözerek açık anahtarı belirliyor, ardından dahili HTTP API üzerinden yalnızca gerekli peer'i getiriyor - Üretimde devreye alındıktan sonra birkaç hafta içinde eski peer sayısı neredeyse ortadan kalktı; gateway'ler daha az durumla daha hızlı peer kurulumu ve yeniden başlatma gerçekleştirebilir hale geldi
Fly.io'nun WireGuard kullanma biçimi
- Fly.io, konteynerleri Firecracker tabanlı VM'lerde çalıştırıyor ve müşteri API'sinin bir parçası gibi WireGuard'ı birçok yerde kullanıyor
flyctl, çalıştığında kendi IPv6 adresine sahip bir TCP/IP stack oluşturuyor ve Fly.io ağındaki Fly Machines ile doğrudan iletişim kuruyor- Bu yaklaşım, remote Docker builder gibi özellikleri aynı LAN üzerindeymiş gibi sunmayı kolaylaştırıyor, ancak bunu güvenilir biçimde sürekli işletmek daha zor
- Fly.io sonunda varsayılan yolu WireGuard-over-WebSockets olarak değiştirdi
Eski gateway provisioning akışı
- Fly.io, dünya genelindeki çeşitli gateway sunucularına gelen WireGuard bağlantılarını uygun özel ağlara bağlıyor
flyctl, konteyner build, SSH konsolu, dosya kopyalama ve servis proxy işlemleri için bir Fly Machine ile iletişim kurması gerektiğinde arka plan agent sürecini çalıştırıyor veya ona bağlanıyor- Agent ilk çalıştığında GraphQL API üzerinden yeni bir WireGuard peer yapılandırması oluşturuyor
- Peer yapılandırması, açık anahtar ve bağlanılacak adresten oluşuyor
- API, ilgili yapılandırmayı NATS mesajlaşma sisteminin RPC mekanizmasıyla uygun gateway'e iletiyor
- Gateway üzerindeki
wggwd, yapılandırmayı alıp SQLite'a kaydediyor, WireGuard Go kütüphanesiyle çekirdeğe ekliyor ve kurulumun tamamlandığını API'ye bildiriyor - API GraphQL isteğine yapılandırmayı döndürdüğünde,
flyctlgateway'e önceden kurulmuş WireGuard peer üzerinden bağlanıyor
Eski yapının yavaşlamasının nedeni
- NATS hızlıydı ama teslim garantisi vermiyordu; bu yüzden güvenilir bir API temeli olarak kullanılması zordu
- Fly.io içeride NATS kullanımını azalttı; örneğin dahili
flydAPI'si NATS tabanlı yapıdan HTTP tabanlı yapıya geçti - NATS kullanımı azalınca WireGuard gateway'leri iyileşti, ancak bu yeterli olmadı
- Fly.io içeride NATS kullanımını azalttı; örneğin dahili
flyctlkapandıktan sonra oluşturulan WireGuard peer'leri gateway üzerinde kalmaya devam etti ve eski peer'leri temizleyen bir süreç yoktu- Ertesi gün yeniden deploy etme veya
fly ssh consoleile debug yapma ihtimali nedeniyle peer'leri silmemeyi seçmişlerdi - Ancak peer'lerin çoğu, kalıcı depolaması olmayan CI işleri tarafından oluşturuluyordu; sonraki çalıştırmada aynı peer ile yeniden bağlanamadıkları için her seferinde yeni peer oluşuyordu
- Ertesi gün yeniden deploy etme veya
- Sonuç olarak gateway'ler yeniden kullanılmayabilecek yüz binlerce peer tutmaya başladı
- Eski peer sayısı arttıkça çekirdekteki WireGuard işlemleri ciddi biçimde yavaşladı
- Özellikle gateway sunucusu yeniden başladıktan sonra tüm peer'leri tekrar çekirdeğe yükleme süreci çok yavaştı
- Hatta bazı kernel panic durumları da yaşandı
Peer'leri yalnızca gerektiğinde çekirdeğe kuran tasarım
- Tüm WireGuard peer geçmişini tek bir SQLite veritabanında tutmak zor değil, ancak tüm peer'leri Linux çekirdeğinde tutmak darboğaz oluşturuyor
- Fly.io, yapılandırmayı gateway'e itmek yerine, gateway'in ihtiyaç duyduğu peer'i API'den istek anında getirdiği bir yaklaşımı seçti
- Yalnızca istemci bağlanmaya çalıştığında peer çekirdeğe eklenirse, eski peer'ler istenildiği anda çekirdekten kaldırılabilir
- Kaldırılmış peer'ler bir sonraki bağlantıda yeniden getirilip kurulabildiği için, gateway'in uzun vadeli durumu sürekli elinde tutma ihtiyacı azalıyor
- Ancak Linux çekirdeğindeki WireGuard için “incoming connection attempt” olayına abone olunabilecek bir API yok
JIT WireGuard peer'lerinin uygulanma biçimi
- Linux çekirdeğinin WireGuard yapılandırma arayüzü Netlink, WireGuard Go kontrol kütüphanesi ise wgctrl-go kullanıyor
- Fly.io, WireGuard bağlantı isteğinin ayırt edilebilir bir paket olmasından yararlanarak BPF filtresi ve packet socket ile doğrudan olay üretiyor
- WebSockets üzerinden geçen WireGuard yolunda ham WireGuard paketlerini almak daha kolay
- Bu yol, kimlik doğrulamasız bir WebSockets bağlantısı üzerinden çerçevelenmiş UDP paketlerini gateway arayüzüyle alışveriş ediyor
- Fly.io bu daemon koduna sahip olduğu için, paket alma fonksiyonuna hook ekleyebiliyor
- WireGuard'da “istemci” ve “sunucu” kavramları yok; trafik göndermeye çalışırken peer'lerin birbirine bağlandığı noktadan noktaya bir protokol var
- İlk bağlanan taraf initiator, karşı taraf responder oluyor
- Fly.io'da genelde initiator
flyctl, responder ise gateway oluyor
- İlk UDP paketi, WireGuard makalesindeki tanıma göre
handshake initiation; paket tipi düz metin olarak 1 byte içinde yazılıyor- Fly.io gelen bağlantıları
udp and dst port 51820 and udp[8] = 1BPF filtresiyle yakalıyor
- Fly.io gelen bağlantıları
Noise handshake içinde peer'i tanımlamak
- WireGuard, Noise Protocol Framework tabanlı ve Noise, handshake sırasında identity hiding için tanımlayıcıları gizliyor
- Bu yüzden paketten kullanıcı adı benzeri bir değeri okuyup doğrudan yapılandırma bulma yöntemi kullanılamıyor
- Fly.io, gelen isteği tanımlayabilmek için Noise şifrelemesinin bir kısmını çalıştırıp kimliği çözüyor
- Bu kod karmaşık ama yaklaşık 200 satır
- Çekirdeğin Netlink arayüzü, yetkili bir sürece arayüzün özel anahtarını verebildiği için gerekli gizli değerler alınabiliyor
- İlgili kod gist olarak paylaşılmış
- Bu süreç sonunda, gateway'e WireGuard bağlantısı kurmaya çalışan kullanıcının açık anahtarını içeren bir olay akışı elde ediliyor
Kurulum, cache ve retry optimizasyonları
- Gateway, SQLite içinde rate limiting cache tutuyor; yeni bir peer gördüğünde dahili HTTP API isteğiyle karşılık gelen peer bilgisini alıp kuruyor
- Bu mantık, gateway'de WireGuard'ı yöneten mevcut küçük daemon'a iyi uydu
- Eski peer'ler artık
cronişiyle agresif biçimde temizlenebiliyor - Yeni peer için yapılan API sorgusu, ilk
handshake initiationmesajına hemen yanıt verecek kadar hızlı olmayabilir- WireGuard hızlıca retry yaptığı için bu durum çalışmayı bozmaz
- Jason Donenfeld'in işaret ettiği Linux WireGuard Netlink özelliğiyle bağlantı daha hızlı kuruluyor
- Gelen initiation mesajından
flyctl'nin geçici source port'unu da içeren 4-tuple adres bilgisi elde ediliyor - Gateway, kendisi initiator,
flyctlise responder'mış gibi peer'i kuruyor - Linux çekirdeği WireGuard bağlantısını
flyctltarafına doğru başlatıyor; protokol sunucu ve istemci rollerine güçlü biçimde bağlı değil - Yeni bağlantı, kurulum hızına çok yakın bir sürede kuruluyor
- Gelen initiation mesajından
Üretimdeki sonuçlar
- Bu yöntem birkaç haftadır production ortamında çalışıyor
- Gateway başına birkaç binden yüz binlerce seviyesine çıkan eski WireGuard peer sayısı neredeyse 0'a indi
- Gateway'in tutması gereken durum azaldı
- Peer kurulumu hızlandı
- Yeniden başlatma sırasında kullanılmayan peer'leri tekrar çekirdeğe yükleme ihtiyacı azaldı
1 yorum
Hacker News görüşleri
Linux çekirdeğindeki WireGuard’da gerektiğinde peer kurma özelliği olmadığı sözü pek anlaşılır gelmiyor. Çalışma zamanında da peer eklenebiliyor gibi görünüyor: https://serverfault.com/questions/1101002/wireguard-client-a...
Doğru anladıysam o aşama zaten geç kalınmış bir aşama; arayüzde eski girdiler kalmasın diye peer eklemeden önce kimlik doğrulaması yapmak istiyorlar gibi
Bu yüzden arayüzün önüne bir eBPF filtresi koyup, kriptografik anahtar yönlendirmesine göre onaylı bir karşı taraf olup olmadığını doğrudan bağlantı kurarak deniyor; geçerse peer’i arayüze ekleyip zaman aşımından sonra kaldıran bir yapı gibi görünüyor
Peer’lerin tamamı SQLite gibi bir yerde durur, istemci bağlanmayı denediğinde gerektiğinde kurulurlar
VPN sağlayıcısı açısından mevcut API biraz kaba. Gerçekte herhangi bir anda kullanılan peer’lerin yalnızca bir kısmı olması bir yana, peer sayısı yüz binlerden on milyonlara çıktığında hepsini tek bir çekirdek örneğinde tutmak başlı başına imkânsız hale gelir
Peer’i önceden kurmak gerekiyorsa, sonuçta belirli bir sunucu makineye bağlanmış olursunuz
Yazıda söylendiği gibi, bugün bile basit bir paket yakalamayla gereken arayüze benzer bir şey yapılabiliyor; Jason’ın API’yi iyi tasarlamış olması sayesinde de sunucu ve istemcinin başlatma yönünü çok kolay tersine çevirebiliyorsunuz. Çekirdek ilk başlatma mesajını atmış olsa bile, kullanıcı bağlantı sorunsuz kuruluyormuş gibi hissediyor
Jann Horn bir adım daha ileri gidip yakalanan başlatma paketini sakladıktan sonra peer kurulunca çekirdeğe yeniden enjekte etmenin de mümkün olduğunu söylemişti; bu da oldukça iyi bir fikir
Bu yazının hayat değiştirecek düzeyde olduğunu düşünmüyorum; daha çok insanların bilse hoşuna gidecek birkaç temiz numaraya benziyor
Bir sonraki adım, bunun üzerine floating peers kurarak peer’i bölgeden tamamen ayırmak. Böylece kullanıcı peer’in hangi region’da ayarlandığını umursamak zorunda kalmaz; bu da yalnızca meraklılara yönelik bir eğlencenin ötesinde gerçek bir ürün avantajı sağlayabilir
JIT WireGuard ifadesi bana biraz tuhaf geliyor. İlk düşüncem “neden? Performans darboğazı şifreleme ve istemci başına JIT bunun için faydalı olmayacak” oldu
Ben olsam doğrudan kullanıcı alanına geçerdim. tokio-uring ya da glommio gibi şeylerle performans alınabilir
Çekirdeğin içinde zorlamaya devam ederseniz Linux milyonlarca aktif tüneli işlemek için tasarlanmadığından sürekli sınırlara çarpacaksınız. Tek bir çekirdekte milyonlarca TCP bağlantısı bile zaman zaman zorlayıcı olabiliyor
Her sınır için bir hack gerekiyor; her hack için de uygulanıp yönetilmesi gereken sistem ayarları ortaya çıkıyor. Linux fiziksel sunucu provisioning araç zinciri, uygulama ve servis geliştirme/konfigürasyon yönetimi araçlarının çok gerisinde
Yoksa ben aptalım da bir şeyi yanlış mı anladım?
Bir Go uygulamasında kullanıcı alanı WireGuard peer’i oluşturmak istiyorsanız, yakın tarihli deneysel proje https://github.com/dpeckett/noisysockets’a bakabilirsiniz
wireguard-go’nun harika çalışmasının üzerine kurulu, ama kütüphane olarak kullanımı daha basit ve Go’ya daha doğal hale getirilmeye çalışıldı
Bununla bir servis mesh yapmak ilginç olabilir. Birden çok dili desteklemek zor olur ama bir socket API’si uygulanabilir gibi
Ancak WireGuard şifrelemesi için donanım hızlandırması henüz görmedim; bu yüzden performans açısından mTLS ile rekabet etmesi zor olabilir
Bu arada şu anda freelance iş arıyorum; yüksek hızlı, güvenli ağlar alanında bir Golang freelancer’a ihtiyacınız varsa iletişime geçebilirsiniz
Rastgele tüneller için Magic Wormhole gibi bir şey; uzun, yüksek bant genişlikli ağlarda dosya aktarımının 20–30 MB/s seviyesinde çökmesi sorununu da ciddi ölçüde iyileştirmesini umuyorum
0 - https://github.com/slackhq/nebula
Tekil uçtan uca mesajlarda, mesaj kuyruğundan geçmektense doğrudan HTTP isteğinin daha güvenilir olabileceğine genel olarak katılıyorum; ama NATS’te bu kadar çok mesajın kaybolup servisi ciddi etkilemiş olması biraz şaşırtıcı
Mesaj kaybolursa NATS başarılı olana kadar yeniden göndermiyor mu? Neden hissedilir düzeyde bir kararsızlık yaşadıklarını bilen var mı, merak ediyorum
NATS’in yapısı sezgisel ve çekici; nerede ters gittiğini merak ediyorum. JetStream’de ayarlanabilir çok sayıda parametre var
Örneğin zaman tabanlı tekrar algılama penceresine sahip bellek stream’i, push/pull yöntemi, yeniden gönderim ve onay politikası ayarları mümkün
Yine de tek seferlik tek mesaj bağlantılarıyla uyumu kötü olabilir. Her hâlükârda daha somut ayrıntılar çok faydalı olurdu
Ama sonuçta bizim için gerekli değildi. Mesaj katmanı ifade gücü katmaktan çok, test ve izlemeyi zorlaştırıyordu
“Eşleri biz initiator’mışız gibi kurup flyctl’i responder olarak bırakıyoruz. Linux çekirdeği WireGuard bağlantısını flyctl tarafına doğru yeniden başlatıyor” kısmı, fiilen el sıkışmaya yarım gidiş-dönüş gecikmesi ekliyor mu?
Örneğin akış 1) flyctl Initiation gönderir, 2) netlink üzerinden eş eklenir ve yeni bir Initiation gönderilir, 3) flyctl Response gönderir şeklinde mi, merak ediyorum
Yani 3. adım yok ya da beklemeye gerek yok; 2. adımdaki yeni başlatmayı engellersen kesinlikle böyle olmaz diye düşünüyorum
1.a) Bob telefonu açmaz ama arayan kimliğindeki numarayı rehberine ekler
“flyctl’i her çalıştırdığımızda sevimli ve devasa CLI’ımız yoktan bir TCP/IP yığını oluşturuyor, kendi IPv6 adresine sahip oluyor ve ağımızda çalışan Fly Machines ile doğrudan iletişim kuruyor” demekle ne kastedildiğini anlamadım
“Yoktan TCP/IP yığını oluşturuyor” denmesinin nedeni, normalde işletim sisteminin TCP/IP yığınını çekirdeğin bir parçası olarak sağlaması
wireguard-go’da TCP/IP yığını kullanıcı alanında çalıştığı için, flyctl komut satırı arayüzü gibi sıradan bir kullanıcı alanı süreci içinde oluşturulabilir
Sistemlerle uzun zamandır uğraşan biri için bu epey sihirli görünebilir. Gerçekten kullanılabilir süreç içi kullanıcı alanı TCP/IP yığınları nispeten yeni ve ilginç şeyler
İlk el sıkışma paketinin ağ yığınına yeniden enjekte edilmesini neyin engellediğini merak ediyorum. Böyle olursa paket kaybı yokmuş gibi olur
Ayrıca eBPF filtresinde
udp[8] = 1kontrolünün amacı da merak konusuYandaki yorumda söylendiği gibi BPF filtresi yalnızca başlatma paketlerini yakalıyor; istenen davranış da bu. TCP bağlantı başlangıcını görmek için SYN dinlemenin WireGuard sürümü
udp[8] = 1yalnızca el sıkışma paketlerini filtreler. O olmadan veri paketleri de kullanıcı alanı daemon’una gönderilirİlk el sıkışmayı tekrar oynatmanın mümkün olup olmadığından emin değilim, ama WireGuard bilinmeyen istemcileri yok saydığı için mümkün olabilir
Varsayılan olarak WireGuard’ı WebSocket üzerinden tünellemesi ilginç. Performans için iyi değil ama flyctl’in kullanıldığı DevOps tarzı işler için sorun olmayabilir
QUIC/HTTP3’ün geleceğini düşünürken de bu tür noktaları merak etmiştim. Ağ operatörlerinin UDP 443 portunu düzgün ele almak yerine tamamen engelleme ihtimali de sıfır değil
flyctliçinde bunun için ayar seçeneği varUDP çalışmazsa hiç çalışmıyor ve debug etmesi de zor oluyor; bu yüzden varsayılanı kesin çalıştığını bildiğimiz tarafta tuttuk
Hangi varsayılanın seçileceği konusundaki tartışmayı kaybetmiş olmam acı verici
Startup’ım Fly’ı neredeyse 1 yıl kullandı. Kodu 1 dakikadan kısa sürede dağıtılmış koda dönüştüren temel özellik gerçekten çok güzel
Backfill için yeni node’ları ayağa kaldırıp indirmek de birkaç saniye sürüyor
Ama şirketin kendisi biraz olgunlaşmamış hissettirdi. Bir keresinde API sunucusu Fly’da 48 saat boyunca erişilemez durumdaydı; bunun benim yapılandırma hatam mı yoksa başka bir “sessiz” kesinti mi olduğundan emin olamadım
“db” ürünü var ama “yönetilen Postgres değil” gibi bir durum; orada da düzenli kopmalar yaşandı
CLI’a Postgres’i üst düzey bir isim olarak ekleyip desteklenen işlev kapsamını sınırlamaları tuhaf geldi
Temel servis API erişimi de sık sık düşüyordu, bu yüzden yeni servis düzeltmesi dağıtımını beklemek zorunda kalıyorduk
Dağıtım deneyimini özlüyorum ama açıkçası şu anda GCP’nin Cloud Run’ından daha memnunum. Çok daha az “sürpriz” var ve dokümantasyon çok daha tamamlanmış
VPS sağlayıcılarının kullanıcıya yönelik backend servis gecikmesini azaltmaya neredeyse hiç destek vermemesi ilginç. Anycast destekleyen yok, GeoDNS seçenekleri de çok az
Ancak GeoDNS ayrı bir karmaşıklık ekliyor
Keşke Fly.io’nun veri aktarım maliyeti daha ucuz olsaydı. Şu anda üzerinde çalıştığım ngrok benzeri serviste Fly.io özelliklerinin önemli bir kısmını beceriksizce yeniden uygulamak zorunda kalıyorum
[0]: https://lastlogin.io
[1]: LastLogin’i dünya çapında dağıtık şekilde çalıştırmak için gereken Fly’a özel kod kabaca şu kadar: https://github.com/lastlogin-io/obligator/blob/37f75cc861f1b...
İyi çalıştığında gerçekten çok pürüzsüzdü, ama bu yeterince sık olmuyordu
Bu fırsatla Netmaker[0]'ı tanıtmak istiyorum
İlgili biri değilim; sadece birden fazla hesap genelinde özel AWS VPC erişimine ihtiyacı olduğu için memnun şekilde kullanan biriyim. Daha yaygın benimsenmesini isterim
[0] https://www.netmaker.io/
Önceki iş yerimde Ansible ile birkaç Windows ve Linux makinede wg kurup yönetmiştik; fena değildi ama sonunda biraz dağınık hale gelmişti
Site fazlasıyla muğlak
“Yüz binlerce peer'i olan bir gateway ve bunların arasında bir daha hiç kullanılmayacak peer'ler” kısmı, ilk paragrafları okurken tam aklımdan geçen düşünceydi
“Gelen bağlantı denemesi olaylarına abone olunan bir API çağrısı yok. Sorun değil. Olayı kendimiz üretiriz. WireGuard bağlantı istekleri pakettir ve kolayca ayırt edilebilir; bu yüzden BPF filtresi ve packet socket ile verimli şekilde yakalanabilir” fikri de güzel
Gelen başlatma mesajını alınca, flyctl'in kullandığı geçici kaynak portu da dahil olmak üzere istenen bağlantının 4'lü adres bilgisi oluşuyor; biz initiator, flyctl ise responder'mış gibi peer'i kurduklarını söylüyorlar. Bunun NAT arkasında da çalışıp çalışmadığını merak ediyorum
{wggwd.fly.io, 12345, clientIP, 23456}gibiYeni bir “initiator” UDP paketi de olsa, çıkan başlatma mesajına verilen yanıt da olsa, yol üzerindeki UDP NAT açısından tamamen aynı görünür
Karar dayanağı yalnızca 4'lü bilgidir ve o 4'lü bilgi aynıdır