- Fly.io ने
flyctlऔर Fly Machines के बीच direct communication बनाए रखते हुए WireGuard gateway पर state का बोझ कम करने के लिए, peers को पहले से install करने के बजाय connection के समय kernel में जोड़ने का तरीका अपनाया - पुराने flow में GraphQL API, NATS RPC के जरिए peer config भेजता था,
wggwdउसे SQLite और Linux kernel WireGuard में register करता था, फिरflyctlconnect करता था - NATS message loss और CI jobs में one-time peer creation के साथ gateway पर लाखों नहीं बल्कि सैकड़ों हज़ार unused peers जमा हो गए, जिससे kernel operations और reboot loading धीमे हो गए
- नए तरीके में
handshake initiationpacket को BPF filter या WebSockets receive path में पकड़कर, Noise handshake के कुछ हिस्से decrypt करके public key identify की जाती है, फिर internal HTTP API से सिर्फ ज़रूरी peer लाए जाते हैं - production में लागू करने के कुछ हफ्तों बाद stale peers लगभग गायब हो गए, और gateway कम state के साथ तेज़ peer setup और reboot संभालने लगा
Fly.io WireGuard का उपयोग कैसे करता है
- Fly.io containers को Firecracker-आधारित VM पर चलाता है, और customer API के एक हिस्से की तरह WireGuard का कई जगह उपयोग करता है
flyctlrun होने पर अपनी IPv6 address वाला TCP/IP stack बनाता है और Fly.io network के Fly Machines से सीधे communicate करता है- यह approach remote Docker builder जैसी सुविधाओं को उसी LAN पर होने जैसा दिखाना आसान बनाती है, लेकिन इसे लगातार स्थिर रूप से चलाना ज़्यादा कठिन है
- अंततः Fly.io ने default path को WireGuard-over-WebSockets में बदल दिया
पुराना gateway provisioning flow
- Fly.io दुनिया भर के कई gateway servers पर आने वाले WireGuard connections को सही private network से जोड़ता है
- जब
flyctlको container build, SSH console, file copy, या service proxy के लिए Fly Machine से communicate करना होता है, तो वह background agent process चलाता है या उससे connect करता है - agent पहली बार चलने पर GraphQL API में नया WireGuard peer config बनाता है
- peer config में public key और connect होने वाला address शामिल होता है
- API उस config को NATS messaging system की RPC के जरिए सही gateway तक भेजती है
- gateway का
wggwdconfig प्राप्त करके उसे SQLite में store करता है, WireGuard Go library से kernel में जोड़ता है, और API को installation complete होने का जवाब देता है - GraphQL request में config लौटने पर
flyctlgateway पर पहले से installed WireGuard peer से connect करता है
पुराना ढांचा धीमा क्यों हुआ
- NATS तेज़ था, लेकिन delivery guarantee नहीं देता था, इसलिए इसे reliable API base के रूप में इस्तेमाल करना कठिन था
- Fly.io ने अंदरूनी तौर पर NATS का उपयोग कम किया, उदाहरण के लिए internal
flydAPI को NATS-based से HTTP-based में बदला गया - NATS का उपयोग घटाने के बाद WireGuard gateway बेहतर हुआ, लेकिन उतना काफ़ी नहीं था
- Fly.io ने अंदरूनी तौर पर NATS का उपयोग कम किया, उदाहरण के लिए internal
flyctlबंद होने के बाद बने WireGuard peers gateway पर बने रहते थे, और stale peers को साफ़ करने की कोई प्रक्रिया नहीं थी- अगली सुबह फिर deploy करने या
fly ssh consoleसे debug करने की संभावना के कारण peers को न हटाने का निर्णय लिया गया था - लेकिन ज़्यादातर peers CI jobs से बनते थे जिनमें persistent storage नहीं होता, इसलिए अगली run में वही peer दोबारा use नहीं हो पाता था और हर बार नया peer बन जाता था
- अगली सुबह फिर deploy करने या
- नतीजतन gateway के पास सैकड़ों हज़ार ऐसे peers हो गए जिनका दोबारा उपयोग नहीं होना था
- stale peers बढ़ने के साथ kernel WireGuard operations बहुत धीमे हो गए
- gateway server reboot के बाद सभी peers को फिर से kernel में load करना खास तौर पर धीमा था
- कुछ kernel panic भी हुए
ज़रूरत पड़ने पर ही peer को kernel में install करने का डिज़ाइन
- सभी WireGuard peer history को एक SQLite में store करना मुश्किल नहीं है, लेकिन सभी peers को Linux kernel में बनाए रखना bottleneck बन जाता है
- Fly.io ने gateway पर config push करने के बजाय, gateway द्वारा API से ज़रूरी peers को on-demand fetch करने का तरीका चुना
- अगर client connect करने की कोशिश करते समय ही peer को kernel में जोड़ा जाए, तो stale peers को कभी भी kernel से हटाया जा सकता है
- हटाए गए peers को अगली connection पर फिर से fetch और install किया जा सकता है, इसलिए gateway को लंबे समय तक state संभालकर रखने की ज़रूरत कम हो जाती है
- हालांकि Linux kernel WireGuard में “incoming connection attempt” event subscribe करने के लिए कोई API नहीं है
JIT WireGuard peer को कैसे implement किया गया
- Linux kernel का WireGuard config interface Netlink है, और WireGuard Go control library wgctrl-go का उपयोग करती है
- Fly.io ने इस तथ्य का उपयोग किया कि WireGuard connection request एक identifiable packet होता है, और BPF filter व packet socket के साथ सीधे event बनाए
- WebSockets WireGuard path में raw WireGuard packets पाना और आसान है
- यह path unauthenticated WebSockets connection के जरिए framed UDP packets को gateway interface के साथ भेजता और प्राप्त करता है
- Fly.io के पास उस daemon का code है, इसलिए packet receive function में hook लगाया जा सकता है
- WireGuard में “client” और “server” का concept नहीं है; traffic भेजते समय peers को जोड़ने वाला यह एक point-to-point protocol है
- पहले connect करने वाला initiator होता है, दूसरा responder
- Fly.io में आमतौर पर
flyctlinitiator और gateway responder होता है
- पहला UDP packet WireGuard paper के अनुसार
handshake initiationहोता है, और packet type plain-text 1 byte में दर्ज होता है- Fly.io आने वाले connection को
udp and dst port 51820 and udp[8] = 1BPF filter से पकड़ता है
- Fly.io आने वाले connection को
Noise handshake में peer की पहचान करना
- WireGuard, Noise Protocol Framework पर आधारित है, और Noise handshake के दौरान identity hiding के लिए identifiers छिपाता है
- इसलिए packet से username जैसी कोई value पढ़कर सीधे config ढूँढने का तरीका काम नहीं करता
- Fly.io incoming request की पहचान करने के लिए Noise encryption का कुछ हिस्सा चलाकर identity decrypt करता है
- यह code पेचीदा है, लेकिन लगभग 200 lines का है
- kernel Netlink interface privileged process को interface की private key दे सकता है, जिससे ज़रूरी secret values मिल जाती हैं
- संबंधित code gist में public है
- इस प्रक्रिया के बाद gateway पर WireGuard connection की कोशिश करने वाले user की public key का event feed मिल जाता है
installation, cache और retry optimization
- gateway SQLite में rate-limit cache बनाए रखता है, और नया peer मिलने पर internal HTTP API request से संबंधित peer जानकारी लाकर उसे install करता है
- यह logic gateway पर WireGuard manage करने वाले छोटे daemon में अच्छी तरह फिट बैठता है
- stale peers को
cronjob से सक्रिय रूप से हटाया जा सकता है - नए peer के लिए API lookup इतना तेज़ नहीं भी हो सकता कि पहले
handshake initiationmessage का तुरंत जवाब दे सके- WireGuard तेज़ी से retry करता है, इसलिए व्यवहारिक रूप से यह समस्या नहीं बनती
- Jason Donenfeld द्वारा बताए गए Linux WireGuard Netlink feature का उपयोग कर connection और तेज़ी से स्थापित किया गया
- incoming initiation message से
flyctlके temporary source port सहित 4-tuple address मिल जाता है - gateway peer को ऐसे install करता है मानो वही initiator हो और
flyctlresponder हो - Linux kernel
flyctlकी तरफ WireGuard connection शुरू कर देता है, और protocol server/client roles पर बहुत ज़्यादा निर्भर नहीं करता - नई connection install होने की गति के लगभग बराबर समय में स्थापित हो जाती है
- incoming initiation message से
production deployment के नतीजे
- यह तरीका कई हफ्तों से production में चल रहा है
- हर gateway पर हज़ारों से लेकर सैकड़ों हज़ार तक stale WireGuard peers की संख्या लगभग 0 के करीब आ गई
- gateway को संभालकर रखने वाली state कम हो गई
- peer setup तेज़ हो गया
- reboot के समय unused peers को फिर से kernel में load करने की ज़रूरत कम हो गई
1 टिप्पणियां
Hacker News की टिप्पणियाँ
यह बात ठीक से समझ नहीं आती कि Linux kernel WireGuard में ज़रूरत पड़ने पर peer install करने की सुविधा नहीं है। ऐसा लगता है कि runtime पर भी peer जोड़े जा सकते हैं: https://serverfault.com/questions/1101002/wireguard-client-a...
अगर मैंने सही समझा है, तो उस चरण तक पहुँचना पहले ही देर हो जाना है, और शायद वे interface में stale entries न बचें इसलिए peer जोड़ने से पहले authenticate करना चाहते हैं
इसलिए ऐसा लगता है कि interface के आगे एक eBPF filter रखा गया है, जो crypto-key routing के आधार पर यह जाँचता है कि सामने वाला approved peer है या नहीं, सीधे connect करके देखता है, और पास होने पर peer को interface में जोड़कर timeout के बाद हटा देता है
सारे peers SQLite जैसी किसी जगह पर रखे जा सकते हैं, और client जब connect करने की कोशिश करे तभी ज़रूरत के हिसाब से install किए जा सकते हैं
VPN provider के नज़रिए से मौजूदा API कुछ भद्दा-सा है। अभी भी किसी समय वास्तव में इस्तेमाल हो रहे peers केवल कुछ ही होते हैं, लेकिन जब peers की संख्या लाखों से बढ़कर करोड़ों तक पहुँच जाए तो सबको एक ही kernel instance में रखना अपने आप में असंभव हो जाता है
अगर peers को पहले से install करना पड़े, तो अंत में आप किसी खास server machine से बँध जाते हैं
जैसा लेख में कहा गया है, अभी भी साधारण packet capture से ज़रूरी interface जैसी चीज़ बनाई जा सकती है, और Jason ने API को इतने अच्छे से design किया है कि server और client की initiation direction को बहुत आसानी से उलटा जा सकता है। भले ही kernel ने पहला initiation message फेंक दिया हो, user को फिर भी connection seamless लगेगा
Jann Horn ने इससे एक कदम आगे जाकर कहा कि captured initiation packet को संभालकर रखा जा सकता था और peer install होने के बाद उसे फिर से kernel में inject किया जा सकता था, और यह भी काफ़ी अच्छा idea है
मुझे नहीं लगता कि यह लेख जीवन बदल देने वाला है; यह ज़्यादा उन कुछ साफ़-सुथरी tricks जैसा है जिन्हें जानना लोगों के काम आ सकता है
अगला कदम इसके आधार पर floating peers बनाना है, ताकि peers को location से पूरी तरह अलग किया जा सके। तब users को इस बात की परवाह नहीं करनी पड़ेगी कि peer किस region में configured है, और यह सिर्फ nerd-fun से आगे बढ़कर असली product benefit भी दे सकता है
JIT WireGuard नाम थोड़ा अजीब लगता है। मेरा पहला ख़याल था, “क्यों? performance bottleneck तो encryption है, और client-specific JIT उससे मदद नहीं करेगा”
मैं होता तो शायद सीधे userspace में चला जाता। tokio-uring या glommio जैसी चीज़ों से performance निकाली जा सकती है
अगर kernel के अंदर ही इसे लगातार धकेलते रहेंगे, तो आप बार-बार limits से टकराएँगे, क्योंकि Linux को लाखों active tunnels सँभालने के लिए बनाया ही नहीं गया है। एक kernel में केवल लाखों TCP connections भी कभी-कभी मुश्किल होते हैं
हर limit पर एक hack चाहिए होगा, और हर hack के साथ कुछ system settings आएँगी जिन्हें लागू और maintain करना होगा। Linux physical server provisioning toolchain, app और service development/configuration management tools की तुलना में काफ़ी पीछे है
या फिर शायद मैं ही बेवकूफ़ हूँ और कुछ ग़लत समझ रहा हूँ?
अगर आप Go app में userspace WireGuard peer बनाना चाहते हैं, तो हाल की experimental project https://github.com/dpeckett/noisysockets भी देख सकते हैं
यह wireguard-go के शानदार काम पर आधारित है, लेकिन इसे library के रूप में इस्तेमाल करने के लिए और सरल तथा Go-जैसा बनाने की कोशिश की गई है
इससे service mesh बनाना दिलचस्प हो सकता है। कई languages को support करना कठिन होगा, लेकिन शायद socket API भी implement किया जा सकता है
हालाँकि WireGuard encryption के लिए hardware acceleration मैंने अभी तक नहीं देखा, इसलिए performance के मामले में इसका mTLS से मुकाबला करना मुश्किल हो सकता है
वैसे, मैं इस समय freelance काम ढूँढ़ रहा हूँ, तो अगर आपको high-speed, secure networking क्षेत्र में Golang freelancer चाहिए, तो मुझसे संपर्क कर सकते हैं
यानी arbitrary tunnels के लिए Magic Wormhole जैसा कुछ, और उम्मीद है कि लंबी high-bandwidth networks पर file transfer का 20~30 MB/s पर गिर जाना भी इससे काफ़ी सुधर सकता है
0 - https://github.com/slackhq/nebula
मैं मोटे तौर पर इस बात से सहमत हूँ कि single point-to-point messaging के लिए message queue से होकर जाने की बजाय direct HTTP request ज़्यादा reliable हो सकती है, लेकिन यह थोड़ा चौंकाने वाला है कि NATS में इतने message loss हुए कि service पर बड़ा असर पड़ा
अगर messages खो जाते हैं, तो क्या NATS success होने तक retransmit नहीं करता? किसी को पता है कि उन्हें इतनी महसूस होने वाली instability क्यों झेलनी पड़ी?
NATS की architecture सीधी-सादी और आकर्षक है, इसलिए जानना अच्छा होगा कि गड़बड़ी कहाँ हुई। JetStream में tune किए जा सकने वाले कई parameters हैं
उदाहरण के लिए time-based deduplication window वाला memory stream, push/pull mode, retransmission और acknowledgement policy settings वगैरह
हालाँकि one-shot single-message connections के साथ इसका तालमेल ठीक न बैठता हो सकता है। किसी भी तरह, अगर और ठोस details हों तो वे बहुत उपयोगी होंगी
लेकिन अंत में हमें इसकी ज़रूरत नहीं थी। message layer ने expressiveness बढ़ाने के बजाय testing और monitoring को ही ज़्यादा कठिन बना दिया
“हम peer को ऐसे सेटअप करते हैं जैसे initiator हम हों, और flyctl को responder रखते हैं। Linux kernel flyctl की तरफ़ WireGuard connection फिर से शुरू कर देता है” — क्या इसका मतलब असल में handshake में आधा round-trip latency जुड़ जाती है?
उदाहरण के लिए, क्या flow कुछ ऐसा है: 1) flyctl Initiation भेजता है, 2) netlink से peer जोड़ा जाता है और नया Initiation भेजा जाता है, 3) flyctl Response भेजता है?
यानी शायद चरण 3 की ज़रूरत नहीं है या उसके लिए इंतज़ार नहीं करना पड़ता, और अगर चरण 2 की नई initiation को रोका जाए तो बात और साफ़ हो जाती है
1.a) Bob फ़ोन नहीं उठाता, लेकिन caller ID वाला नंबर अपनी address book में जोड़ लेता है
“हर बार जब आप flyctl चलाते हैं, हमारी प्यारी और विशाल CLI हवा से एक TCP/IP stack बना देती है, उसका अपना IPv6 address होता है, और वह हमारे नेटवर्क पर चल रही Fly Machines से सीधे बात करती है” — इसका मतलब समझ नहीं आया
“हवा से TCP/IP stack बना देती है” ऐसा इसलिए कहा गया, क्योंकि आम तौर पर operating system TCP/IP stack को kernel के हिस्से के रूप में देता है।
wireguard-go में TCP/IP stack user space में चलता है, इसलिए इसे flyctl command-line interface जैसे सामान्य user-space process के अंदर बनाया जा सकता है।
जो लोग बहुत समय से systems के साथ काम कर रहे हैं, उन्हें यह काफ़ी जादुई लग सकता है। सच में उपयोगी process के अंदर चलने वाले user-space TCP/IP stack अपेक्षाकृत नए और काफ़ी नवीन हैं
मैं जानना चाहता हूँ कि शुरुआती handshake packet को network stack में दोबारा inject होने से रोकने वाली चीज़ क्या है। ऐसा हो तो शायद packet loss न हो।
और eBPF filter में
udp[8] = 1चेक करने का उद्देश्य भी जानना हैजैसा बगल वाली टिप्पणी में कहा गया, BPF filter सिर्फ़ initiation packet को पकड़ता है, और वही वांछित व्यवहार है। यह WireGuard का वैसा संस्करण है जैसे TCP connection start देखने के लिए SYN sniff करना
udp[8] = 1सिर्फ़ handshake packet को filter करता है। इसके बिना data packet भी user-space daemon को भेज दिए जाएँगे।शुरुआती handshake को replay किया जा सकता है या नहीं, इस पर मैं निश्चित नहीं हूँ, लेकिन संभव है, क्योंकि WireGuard अनजान clients को नज़रअंदाज़ करता है
यह दिलचस्प है कि default रूप से WireGuard को WebSocket के ऊपर tunnel किया जाता है। performance के लिए तो अच्छा नहीं होगा, लेकिन flyctl जिन DevOps तरह के कामों में इस्तेमाल होता है, उनके लिए ठीक लगता है।
QUIC/HTTP3 के भविष्य के बारे में सोचते समय भी मैं ऐसी चीज़ों को लेकर उत्सुक था। इस बात की संभावना शून्य नहीं है कि network operators UDP 443 port को सही तरह से संभालने के बजाय उसे पूरी तरह block कर दें
flyctlमें उसके लिए configuration option भी है।अगर UDP काम न करे तो चीज़ बस चलती ही नहीं, और debug करना भी मुश्किल होता है, इसलिए default हमने उसी विकल्प को रखा जिसके बारे में हमें पक्का पता है कि वह काम करेगा।
कौन-सा default चुना जाए, इस बहस में मैं हार गया — यह बात थोड़ी कड़वी लगती है
मेरे startup ने लगभग 1 साल तक Fly इस्तेमाल किया। code को 1 मिनट से कम समय में deployed code में बदल देने वाली इसकी मुख्य सुविधा सच में बहुत खूबसूरत है।
backfill के लिए नया node ऊपर लाना और हटाना भी कुछ ही सेकंड में हो जाता था।
लेकिन कंपनी खुद थोड़ी अपरिपक्व लगी। एक बार API server Fly पर 48 घंटे तक उपलब्ध नहीं था, और मुझे यक़ीन नहीं हुआ कि यह मेरी config की गलती थी या कोई और “silent” outage।
“db” product है, लेकिन बात कुछ ऐसी है कि “यह managed Postgres नहीं है”, और वहाँ भी रुकावटें लगातार होती रहीं।
CLI में Postgres को top-level noun के रूप में जोड़ना, लेकिन supported features की सीमा तय कर देना, अजीब लगा।
core service API access भी अक्सर बंद हो जाता था, इसलिए नई service changes deploy करने के लिए इंतज़ार करना पड़ता था।
deployment अनुभव याद आता है, लेकिन ईमानदारी से कहूँ तो अभी मैं GCP के Cloud Run से ज़्यादा संतुष्ट हूँ। “surprises” बहुत कम हैं और documentation भी कहीं अधिक परिपक्व है
यह हैरानी की बात है कि VPS providers users के लिए backend service latency कम करने में लगभग कोई मदद नहीं करते। Anycast support कहीं नहीं मिलता, और GeoDNS options भी बहुत कम हैं।
हालांकि GeoDNS अपनी अलग complexity जोड़ता है।
काश Fly.io की data transfer cost थोड़ी सस्ती होती। अभी मैं जिस ngrok-जैसी service पर काम कर रहा हूँ, उसमें मुझे Fly.io की कई सुविधाओं को काफ़ी असुविधाजनक तरीके से फिर से implement करना पड़ रहा है।
[0]: https://lastlogin.io
[1]: LastLogin को globally distributed तरीके से चलाने के लिए ज़रूरी Fly-specific code लगभग इतना ही है: https://github.com/lastlogin-io/obligator/blob/37f75cc861f1b...
जब यह ठीक चलता था, तब सच में बहुत smooth था, लेकिन उतनी बार नहीं कि उस पर भरोसा किया जा सके
इस मौके पर Netmaker[0] का परिचय कराना चाहता हूँ
मैं इससे जुड़ा हुआ नहीं हूँ, बस कई accounts में फैले private AWS VPC access की ज़रूरत होने पर इसे संतोषजनक तरीके से इस्तेमाल करने वाला एक यूज़र हूँ। अच्छा होगा अगर इसे और व्यापक रूप से अपनाया जाए
[0] https://www.netmaker.io/
पिछली नौकरी में हमने Ansible से कुछ Windows और Linux machines पर wg configure और manage किया था; ठीक था, लेकिन अंत तक थोड़ा बिखर गया था
साइट बहुत अस्पष्ट है
“लाखों peers वाला gateway, जिनमें से कुछ peers फिर कभी इस्तेमाल नहीं होंगे” वाला हिस्सा, शुरुआती पैराग्राफ़ पढ़ते समय मेरे दिमाग़ में बिल्कुल यही बात आई थी
“incoming connection attempt events को subscribe करने के लिए कोई API call नहीं है। कोई बात नहीं। हम ख़ुद event बना लेंगे। WireGuard connection request packets होते हैं और आसानी से पहचाने जा सकते हैं, इसलिए उन्हें BPF filter और packet socket से efficiently पकड़ा जा सकता है” — यह विचार भी अच्छा लगा
incoming initiation message मिलने पर, flyctl द्वारा इस्तेमाल किए गए temporary source port सहित इच्छित connection का 4-tuple address मिल जाता है, और फिर वे peer को ऐसे install करते हैं मानो हम initiator हों और flyctl responder हो; यह NAT के पीछे भी काम करता है या नहीं, यह जानने की जिज्ञासा है
{wggwd.fly.io, 12345, clientIP, 23456}जैसा रूपचाहे वह नया “initiator” UDP packet हो, या outgoing initiation message का response, रास्ते में मौजूद UDP NAT को दोनों बिल्कुल एक जैसे दिखते हैं
क्योंकि निर्णय का आधार सिर्फ 4-tuple है, और वह 4-tuple वही रहता है